8. Multipage apps
8. Multipage apps
In deze oefeningenreeks oefen je op:
- Het maken van apps met meerdere pagina's op een onderhoudbare manier
- Het maken van custom components met attributen en events
Voor deze oefeningen zijn startbestanden voorzien, je vindt deze op GitPub.
Startbestanden
Oefening 1
Voor deze oefening beginnen we met het maken van een app met enkele pagina's en het gebruik van de router. We maken ook gebruik van een navbar.
De startbestanden voor deze oefening bevatten een template zoals je zal krijgen op het examen (hier komen nog uitbreidingen op in les 8). Gebruik deze om een app te maken met 3 pagina's:
- Een homepage
- Een pagina 'Pokémon'
- Een pagina 'Trainers'
De html van de pagina's is telkens gegeven. Je moet hier nog wel aanpassingen aan doen om de links en navigatiebar te laten werken. De layout van de kaart waarin je data toont doet er niet veel toe. Je mag gerust het simpel houden of een AI vragen de layout ervan te ontwerpen. De data haal je uit de datamanager.
Zorg ook voor een navigatiebar die getoond wordt op alle pagina's en die router gebruikt om te navigeren. Pas ook de andere links op de homepagina aan zodat deze werken met de router.
Screenshots:



Oefening 2
In deze oefening ga je een deel van de Pokémon website ombouwen naar een dynamische app. Vertrek van je resultaat van oefening 1.
- Maak een custom element voor de card van 1 Pokémon, zorg dat dit custom attributen heeft voor alle properties van een Pokémon.
- Vervang de code om Pokémons te tonen zodat deze gebruik maakt van het custom element.
- Voorzie op dat element ook een delete knop.
- Zorg dat de delete werkt met een custom event.
Uitbreidingen:
- Doe dit ook voor de trainers als herhaling.
- Maak een extra pagina/model/... bij voor de verschillende regions.
- Gebruik data uit een online API als startdata voor je datamanager.
- ...

Optioneel & uitdagend Oefening 3
Notitie
Voor deze oefening zijn de concepten van objectgeörienteerd programmeren, abstracte klassen, en recursie nodig. Als je de cursus objectgeörienteerd programmeren nog niet volledig afgelegd hebt, sla je deze oefening best over.
In deze oefening breid je de Tic-Tac-Toe oefening uit met een extra spel, Connect Four (vier-op-een-rij).
Home pagina
Begin met een homepagina toe te voegen aan je oplossingen van vorige les. Maak hiervoor gebruik van de HTML en CSS-code uit de startbestanden.
De "Play Now" links moeten naar de "/tic-tac-toe" en "/connect-four" pagina's leiden.

Tic-Tac-Toe pagina & Navigatiebalk
Kopieer je Tic-Tac-Toe code van vorige les naar de nieuwe "/tic-tac-toe" pagina. Schrijf vervolgens een custom-element voor een navigatiebalk en voeg hierin links naar de homepagina, de connect-four pagina, en de tic-tac-toe pagina.

ConnectFour klasse
Doorheen deze oefeningen bouw je een implementatie van het MiniMax algoritme voor het spel Connect Four (Vier-op-een-rij).
GamePiece
Schrijf twee nieuwe subklassen van de GamePiece klasse, Red en Yellow. Je kan onderstaande emojis gebruiken om de verschillende spelstukken voor te stellen:
- 🔴 voor rood
- 🟡 voor geel
ConnectFour
Schrijf een nieuwe subbklasse van de BoardGame klasse, ConnectFour. Initialiseer het spelbord met 7 rijen en kolommen.
Hieronder worden de methodes beschreven waar extra uitleg voor nodig is, implementeer de andere methodes volgens de regels van Vier-op-een-rij.
applyMove
De applyMove methode is grotendeels hetzelfde als die voor Tic-Tac-Toe, maar je kan nu best ook de laatste uitgevoerde zet bijhouden. Doe je dit niet, dan moet je in de getWinner methode telkens het hele bord controleren op vier-op-een-rij, wat erg inefficiënt is. Als je de laatste zet kent, kan moet je enkel de rij, kolom en diagonalen controleren die door deze zet gaan.
Aangezien de applyMove methode zeer vaak opgeroepen wordt tijdens het MiniMax algoritme, kan je niet één instantievariabele bijhouden met de laatste zet want het algoritme werkt met backtracking en gaat dus vaak terug naar een vorig gegenereerd bord. Bewaar de laatste zetten in een map waarbij de key een GameBoard is en de bijhorende waarde de zet die genomen is om tot dat spelbord te komen.
Deze map wordt snel heel groot en zal dus ook bijzonder veel geheugen gebruiken, dit is verre van ideaal. Zorg er dus voor dat je de map reset in de applyMove methode wanneer je hier detecteert dat de zet echt is, i.e. uitgevoerd wordt op het echte bord en niet op een gegenereerd bord tijdens het MiniMax algoritme.
getAvailableMoves
Om de beschikbare zetten te genereren, kan je natuurlijk alle vakken van het spelbord overlopen en de geldige vakken teruggeven. Dit is echter niet efficiënt aangezien enkel de onderste rij van elke kolom beschikbaar is (tenzij deze al vol is). Zorg er dus voor dat je de lus tijdig onderbreekt, zodra je in een kolom een geldige zet gevonden hebt, moet je niet verder zoeken. Je begint best ook van onder naar boven te zoeken, aangezien de onderste vakken van een kolom het vaakst beschikbaar zijn. Naarmate het spel vordert, is dit niet meer het geval, maar dan wordt de zoekboom (van MiniMax) ook kleiner wet compenseert. Eventueel zou je het aantal resterende zetten kunnen tellen en als dit kleiner is dat de helft van het aantal vakken, van boven naar beneden beginnen te zoeken.
getWinner
De getWinner methode moet zoeken door de rij, kolom en diagonalen die door de laatste zet gaan, op zoek naar vier-op-een-rij. Om het startpunt van de twee diagonalen te bepalen, kan je gebruik maken van onderstaande hulpfunctie:
import type {Position} from '../boardgame/types/gamePiece.ts'
/**
* Calculate the starting coordinates of the main (\) and anti (/) diagonals that pass through the given cell.
* This coordinates starts at the bottom of the board, and thus at the highest possible row index.
*
* @param row The row coordinate of the cell for which the diagonals must be calculated.
* @param column The column coordinate of the cell for which the diagonals must be calculated.
* @param size The total number of rows (or columns) of the n x n board.
*/
export function getDiagonalStartCoordinates(row: number, column: number, size: number): [Position, Position] {
const maxIndex = size - 1
// The \ diagonal.
const mainDelta = Math.min(maxIndex - row, maxIndex - column)
const mainStart: Position = [row + mainDelta, column + mainDelta]
// The / diagonal.
const antiDelta = Math.min(maxIndex - row, column)
const antiStart: Position = [row + antiDelta, column - antiDelta]
return [mainStart, antiStart]
}ConnectFourMiniMax klasse
Schrijf een nieuwe subklasse van de MiniMax klasse, ConnectFourMiniMax.
In de TicTacToeMiniMax klasse hebben we de maxDepth van 9 gebruikt, dit was daar ideaal aangezien er maximaal 9 zetten mogelijk zijn in een spelletje Tic-Tac-Toe. In Connect Four is dit echter niet het geval, als we, zoals hierboven vermeld, een 7x7 bord gebruiken, zijn er maximaal 49 zetten mogelijk. Dit is veel te groot om volledig te doorzoeken, er zijn namelijk niet slechts 49 opties die doorzocht moeten worden. Stel dat de AI begint, dan zijn er 7 mogelijke zetten, elk van deze zetten leidt tot 7 andere zetten van de tegenstander, elk van deze zetten leidt tot 7 andere zetten van de AI, enzovoort (tot een kolom volledig vol is, dan wordt het aantal mogelijke zetten verminderd naar 6).
Om deze reden is het belangrijk om de maxDepth lager te zetten, in de oplossingen is een maxDepth van 6 gebruikt. Maar als je merkt dat dit nog te traag is op jouw hardware, kan je dit verder verlagen, wat natuurlijk de kwaliteit van de AI verder vermindert.
evaluateBoard
De evaluateBoard methode is ook een stuk complexer dan die van Tic-Tac-Toe, aangezien er veel meer manieren zijn om te winnen.
Het bestand winningCombinations.ts uit de startbestanden bevat een array van alle mogelijke winnende combinaties in een 7x7 bord. Gebruik deze array om, aan de hand van onderstaande functie, een score te berekenen voor een gegeven bord. De score van een bord is de som van de scores van alle mogelijke winnende combinaties.
De scoreWindow functie krijgt een array van 4 vakken (een mogelijke winnende combinatie) en berekent hier een score voor.
function scoreWindow(window: (Player | null)[]): number {
let score = 0
const computerPieces = window.filter(p => p === 'Computer').length
const playerPieces = window.filter(p => p === 'Player').length
const empty = window.filter(p => p === null).length
if (computerPieces === 4) return 10000
if (computerPieces === 3 && empty === 1) score += 500
if (computerPieces === 2 && empty === 2) score += 50
if (playerPieces === 3 && computerPieces === 1) score += 100
if (playerPieces === 2 && computerPieces === 1) score += 25
// Since the player shouldn't win, this must be blocked at all costs and must therefor have a high penalty.
if (playerPieces === 4) return -100000
if (playerPieces === 3 && empty === 1) score -= 800
if (playerPieces === 2 && empty === 2) score -= 50
return score
}Aangezien de middelste kolom het meest waardevol is (omdat deze deel uitmaakt van de meeste winnende combinaties), kan je ook een extra score geven aan vakken in deze kolom. Ga door elke rij in de middelste kolom en verhoog de score met 25 voor elk vak dat bezet is door een computerstuk, en verlaag de score met 25 voor elk vak dat bezet is door een spelersstuk.
Connect Four pagina
Voeg een nieuwe pagina toe aan de app die getoond wordt als de gebruiker de "/connect-four" route bezoekt. Onderstaande vide demonstreert de voorlopige werking van deze pagina.
- pruning
Zoals in bovenstaande video te zien is, duurt het relatief lang voordat de AI een zet berekent heeft. Via - pruning kunnen we de snelheid aanzienlijk verbeteren.
De waarde stelt de minimale score voor die de maximizer (de computer) kan garanderen, ongeacht de zetten van de tegenstander. De waarde stelt de maximale score voor die de minimizer (de speler) kan garanderen, ongeacht de zetten van de tegenstander. Tijdens het doorzoeken van de zoekboom, kunnen we alle takken negeren waarvoor de speler gegarandeerd een slechtere minimale score heeft dan de computer. De speler zou deze takken immers nooit kiezen, aangezien er betere opties beschikbaar zijn. Door deze takken te negeren, kunnen we de zoekboom aanzienlijk verkleinen en de snelheid van het algoritme verbeteren.
Probeer deze optimalisatie zelf te implementeren aan de hand van de pseudocode op Wikipedia, lukt dit niet, dan kan je onderstaande code gebruiken.
#minimax function with alpha-beta pruning
/**
* @param board The board for which to determine the best move.
* @param depth The maximum number of moves in the future to search for. A larger depth results in a more accurate
* result, but als is much longer computer times.
* @param isMaximizingPlayer Whether or not the current player is the maximizing player (the computer).
* This should be true when the function is first called, and should be flipped for each recursive call.
* @param alpha The minimum score that the maximizing player is assured of.
* @param beta The maximum score that the minimizing player is assured of.
* @private
*/
#minimax(
board: GameBoard,
depth: number,
isMaximizingPlayer: boolean,
alpha: number = -Infinity,
beta: number = Infinity,
): MiniMaxResult {
const moves = this.game.getAvailableMoves(isMaximizingPlayer ? 'Computer' : 'Player', board)
if (depth === 0 || moves.length === 0) {
return {score: this.evaluateBoard(board)}
}
const fn = isMaximizingPlayer ? Math.max : Math.min
const result: MiniMaxResult = {
score: isMaximizingPlayer ? -Infinity : Infinity,
}
for (const move of moves) {
const nextBoard = board.map(row => [...row])
this.game.applyMove(move, nextBoard)
const newScore = fn(
result.score,
this.#minimax(nextBoard, depth - 1, !isMaximizingPlayer, alpha, beta).score,
)
if (result.score !== newScore) {
result.score = newScore
result.move = move
}
if (isMaximizingPlayer) {
alpha = Math.max(alpha, result.score)
} else {
beta = Math.min(beta, result.score)
}
// If the maximum score of the minimizing player is less than the minimum score of the maximizing player,
// then the maximizing player will never allow this to happen (assuming they are a good player).
if (beta <= alpha) {
break
}
}
return result
}Zoals in onderstaande video te zien is, is de snelheid nu aanzienlijk beter. Zelfs als de maxDepth verhoogd wordt naar 7, is deze AI nog steeds sneller dan de vorige AI met een maxDepth van 6.