Onderaan deze pagina staat een grote lap code. Kopieer deze naar je editor, sla het bestand op als “zeeslag.htm”, run het, en speel even met de pagina die je zojuist gekregen hebt. Het is een min of meer werkende versie van het spel “zeeslag” of “battleships” dat je wellicht als kind gespeeld hebt.
Jouw opdracht als scripter is om het spel af te maken. Sommige belangrijke functionaliteit ontbreekt namelijk. Boten kunnen bijvoorbeeld alleen verticaal geplaatst worden, het spel weet niet wanneer er een winnaar is, en zo zijn er nog wat functies die we graag terug zouden zien.
Je cijfer wordt gebaseerd op hoeveel functionaliteit er in je spel zit en hoe je code eruit ziet. Denk daarom aan naamgeving, inspringen, het gebruiken van kleine functies, het gebruiken van comments, etc.
Lever aan het einde je code als html bestand in via de email.
Uiteindelijk moet het ook in je isas worden geupload.
Opdracht 1: Verkenning
Hier is de powerpoint van de les, die kan je helpen.
1a) In de drawMySea() functie (die onder aan het script wordt aangeroepen) zitten twee gekke for-loops waarin er steeds twee variabelen geïnitialiseerd en opgehoogd worden. Wat is x? Wat is y? Wat is rowNumber? Wat is columnNumber?
1b) Waarom worden x en y niet met ++ opgehoogd maar met squareSize en gapSize?
1c) Wat doet de createFriendlyGridSquare() functie die in drawMySea() wordt aangeroepen?
1d) De createFriendlyGridSquare() functie heeft drie parameters. x en y spreken misschien voor zich, maar wat zit er in dat gridNumber? (Je kunt dit met een console log uitproberen)
1e) Wat gebeurt er als de draw methode van een “square” wordt uitgevoerd?
1f) Wanneer wordt de gameSetupRound(clickEvent) functie uitgevoerd? Wat zit er in die “clickEvent” parameter?
1g) Wanneer wordt de playRound(clickEvent) functie uitgevoerd?
1h) Wat voor dingen zitten er in de mySquares array? Wat voor dingen zitten er in de enemySquares array? Waar is insertEnemyGameMethods(gridNumber) voor nodig?
1i) Waar zou currentShipIndex voor dienen?
1j) Waarom zou de maker van de opdracht de moeite hebben genomen om de getSquareSize() functie te maken?
[ Mail je antwoorden op vragen 1a t/m 1j… ]
1k Lees de opdrachten 2 t/m 9 aandachtig. Lees opdracht 10 als je daar zin in hebt. Probeer tijdens het lezen van iedere opdracht een soort plannetje te bedenken. Dat zou vragen kunnen opleveren die we in de les kunnen beantwoorden.
- Zijn er onduidelijkheden in (of meerdere interpretaties van) de opdracht zelf?
- Zijn er dingen lastig of merkwaardig in hoe de code die je hebt werkt?
- Twijfel je over verschillende manieren waarop je het zou kunnen aanpakken?
- Heb je wel een oplossing in gedachten, maar weet je niet zeker hoe je dat in Javascript kan opschrijven?
[ Mail je vragen over opdrachten 2 t/m 9 (of 10)… ]
Opdracht 2: Verduidelijken
Plaats instructies bij het spel, zodat altijd duidelijk is wat de speler moet doen. Bijvoorbeeld: Plaats nu de boot van 4 vakjes, gooi nu een bom, etc).
Lees eerst de volgende opdrachten goed door. Misschien vind je bepaalde opdrachten moeilijker dan anderen. Maak eerst de opdrachten die je makkelijk lijken, concentreer je daarna pas op de opdrachten waar je verwacht meer tijd voor nodig te hebben.
Opdracht 3: Dubbelop
Zorg ervoor dat er niet twee keer op dezelfde plek een bom gegooid kan worden (ook niet door de computer).
Opdracht 4: Meer objecten!
Deze opdracht gaat over objecten. Hoe maak je ookalweer een object? Wat is een veld? Wat is een methode?
4a) Verander de “ships” array (waar nu integers die de lengte van het schip aangeven in zitten) in een array van objecten. Naast het formaat van de boot moet er ook een veld met het type boot (cruiser, aircraft carrier, destroyer, minesweeper, submarine) in de objecten zitten. We doen dit omdat het in latere opdrachten handig is dat deze informatie bij elkaar wordt opgeslagen.
Hint: (voor als je het moeilijk vindt, er zijn meer manieren): maak eerst een variabele voor elk schip. Stop in elke variabele een object dat dat schip beschrijft, met de juiste velden erin. Stop daarna deze variabelen in de array, in plaats van de integers die er nu in zitten.
4b) Zorg ervoor dat bij het plaatsen van de boot, de naam van de boot ook in het object in de mySquares en enemySquares arrays wordt opgeslagen.
Hint: Dit is dus een ander object. Welke informatie zit er nog meer in dit object?
4c) Controleer bij het raken van een boot, of er nog squares zijn die dezelfde boot bevatten die nog niet geraakt zijn.
Hint: Waar in de code weet je of er een object geraakt is? Welke informatie heb je nodig om te controleren of er nog meer squares zijn die dezelfde boot bevatten die nog niet geraakt zijn? Waar kun je die informatie vinden?
4d) Geef op een of andere manier aan welke boten er al gekelderd zijn.
Opdracht 5: Horizontaal
5a) Zorg ervoor dat schepen ook horizontaal geplaatst kunnen worden. Misschien heb je een rotate knop of iets dergelijks nodig.
5b) Zorg ervoor dat schepen bij een horizontale plaatsing niet illegaal geplaatst kunnen worden (buiten de kaders of over andere schepen heen)
Opdracht 6: Winnaar
Zorg ervoor dat het spel een melding geeft als er een winnaar is. Het is handig om eerst opdracht 4 te doen, maar als die niet lukt is het niet strikt noodzakelijk om deze functionaliteit in het spel te krijgen.
Hint: Welke informatie moet je hebben om te weten of er een winnaar is? Maak een functie die die informatie verzameld en controleert en geef een boolean terug. Wanneer roep je deze functie aan?
Opdracht 7: Weerstand
Zorg ervoor dat het spel van de tegenstander de boten random plaatst (ipv via de voorgekookte json string). Het moet natuurlijk wel een legale plaatsing zijn. Plaats de boot opnieuw als deze niet legaal is. Misschien kun je wat functies hergebruiken?
Opdracht 8: Canvas
Zorg ervoor dat als er raak geschoten wordt er niet alleen het vierkantje van kleur verandert, maar dat er bovendien een sterretje in het grid getekend wordt (met het canvas). Bij een misser moet het een kruis zijn.
Opdracht 9: A.I.
Zorg ervoor dat de computer wat slimmer wordt door als er een bom raak gegooid is, vervolgens het gebied rondom de “hit” te laten verkennen.
Opdracht 10: Multiplayer (voor de gaaf)
Geef het spel een multiplayer mogelijkheid met firebase (je moet nog steeds tegen de computer kunnen spelen)
DE CODE
<!DOCTYPE html> <html> <head> <title>Mijn Zeeslag</title> </head> <body> <canvas id="mySea" width=900 height=900></canvas> <canvas id="enemySea" width=800 height=800></canvas> <div>Hier moet een aanwijzing</div> <script> //EnemySquares array met coordinaten, maar nog zonder de methoden. (JSON.parse maakt van een json string een array van javascript objecten.) var enemySquares = JSON.parse('[{"x":0,"state":"water","y":0,"id":0},{"x":0,"state":"water","y":50,"id":1},{"x":0,"state":"water","y":100,"id":2},{"x":0,"state":"water","y":150,"id":3},{"x":0,"state":"water","y":200,"id":4},{"x":0,"state":"water","y":250,"id":5},{"x":0,"state":"water","y":300,"id":6},{"x":0,"state":"water","y":350,"id":7},{"x":0,"state":"water","y":400,"id":8},{"x":0,"state":"water","y":450,"id":9},{"x":0,"state":"water","y":500,"id":10},{"x":0,"state":"water","y":550,"id":11},{"x":50,"state":"water","y":0,"id":12},{"x":50,"state":"water","y":50,"id":13},{"x":50,"state":"ship","y":100,"id":14},{"x":50,"state":"ship","y":150,"id":15},{"x":50,"state":"water","y":200,"id":16},{"x":50,"state":"water","y":250,"id":17},{"x":50,"state":"water","y":300,"id":18},{"x":50,"state":"water","y":350,"id":19},{"x":50,"state":"water","y":400,"id":20},{"x":50,"state":"water","y":450,"id":21},{"x":50,"state":"water","y":500,"id":22},{"x":50,"state":"water","y":550,"id":23},{"x":100,"state":"water","y":0,"id":24},{"x":100,"state":"water","y":50,"id":25},{"x":100,"state":"water","y":100,"id":26},{"x":100,"state":"water","y":150,"id":27},{"x":100,"state":"water","y":200,"id":28},{"x":100,"state":"ship","y":250,"id":29},{"x":100,"state":"ship","y":300,"id":30},{"x":100,"state":"ship","y":350,"id":31},{"x":100,"state":"water","y":400,"id":32},{"x":100,"state":"water","y":450,"id":33},{"x":100,"state":"water","y":500,"id":34},{"x":100,"state":"water","y":550,"id":35},{"x":150,"state":"water","y":0,"id":36},{"x":150,"state":"water","y":50,"id":37},{"x":150,"state":"water","y":100,"id":38},{"x":150,"state":"water","y":150,"id":39},{"x":150,"state":"water","y":200,"id":40},{"x":150,"state":"water","y":250,"id":41},{"x":150,"state":"water","y":300,"id":42},{"x":150,"state":"water","y":350,"id":43},{"x":150,"state":"water","y":400,"id":44},{"x":150,"state":"water","y":450,"id":45},{"x":150,"state":"water","y":500,"id":46},{"x":150,"state":"water","y":550,"id":47},{"x":200,"state":"water","y":0,"id":48},{"x":200,"state":"water","y":50,"id":49},{"x":200,"state":"ship","y":100,"id":50},{"x":200,"state":"ship","y":150,"id":51},{"x":200,"state":"ship","y":200,"id":52},{"x":200,"state":"ship","y":250,"id":53},{"x":200,"state":"ship","y":300,"id":54},{"x":200,"state":"water","y":350,"id":55},{"x":200,"state":"water","y":400,"id":56},{"x":200,"state":"water","y":450,"id":57},{"x":200,"state":"water","y":500,"id":58},{"x":200,"state":"water","y":550,"id":59},{"x":250,"state":"water","y":0,"id":60},{"x":250,"state":"water","y":50,"id":61},{"x":250,"state":"water","y":100,"id":62},{"x":250,"state":"water","y":150,"id":63},{"x":250,"state":"water","y":200,"id":64},{"x":250,"state":"water","y":250,"id":65},{"x":250,"state":"water","y":300,"id":66},{"x":250,"state":"water","y":350,"id":67},{"x":250,"state":"water","y":400,"id":68},{"x":250,"state":"water","y":450,"id":69},{"x":250,"state":"water","y":500,"id":70},{"x":250,"state":"water","y":550,"id":71},{"x":300,"state":"water","y":0,"id":72},{"x":300,"state":"water","y":50,"id":73},{"x":300,"state":"water","y":100,"id":74},{"x":300,"state":"water","y":150,"id":75},{"x":300,"state":"water","y":200,"id":76},{"x":300,"state":"water","y":250,"id":77},{"x":300,"state":"water","y":300,"id":78},{"x":300,"state":"water","y":350,"id":79},{"x":300,"state":"water","y":400,"id":80},{"x":300,"state":"water","y":450,"id":81},{"x":300,"state":"water","y":500,"id":82},{"x":300,"state":"water","y":550,"id":83},{"x":350,"state":"water","y":0,"id":84},{"x":350,"state":"water","y":50,"id":85},{"x":350,"state":"water","y":100,"id":86},{"x":350,"state":"water","y":150,"id":87},{"x":350,"state":"water","y":200,"id":88},{"x":350,"state":"ship","y":250,"id":89},{"x":350,"state":"ship","y":300,"id":90},{"x":350,"state":"ship","y":350,"id":91},{"x":350,"state":"water","y":400,"id":92},{"x":350,"state":"water","y":450,"id":93},{"x":350,"state":"water","y":500,"id":94},{"x":350,"state":"water","y":550,"id":95},{"x":400,"state":"water","y":0,"id":96},{"x":400,"state":"water","y":50,"id":97},{"x":400,"state":"water","y":100,"id":98},{"x":400,"state":"water","y":150,"id":99},{"x":400,"state":"water","y":200,"id":100},{"x":400,"state":"water","y":250,"id":101},{"x":400,"state":"water","y":300,"id":102},{"x":400,"state":"water","y":350,"id":103},{"x":400,"state":"water","y":400,"id":104},{"x":400,"state":"water","y":450,"id":105},{"x":400,"state":"water","y":500,"id":106},{"x":400,"state":"water","y":550,"id":107},{"x":450,"state":"water","y":0,"id":108},{"x":450,"state":"water","y":50,"id":109},{"x":450,"state":"ship","y":100,"id":110},{"x":450,"state":"ship","y":150,"id":111},{"x":450,"state":"ship","y":200,"id":112},{"x":450,"state":"ship","y":250,"id":113},{"x":450,"state":"water","y":300,"id":114},{"x":450,"state":"water","y":350,"id":115},{"x":450,"state":"water","y":400,"id":116},{"x":450,"state":"water","y":450,"id":117},{"x":450,"state":"water","y":500,"id":118},{"x":450,"state":"water","y":550,"id":119},{"x":500,"state":"water","y":0,"id":120},{"x":500,"state":"water","y":50,"id":121},{"x":500,"state":"water","y":100,"id":122},{"x":500,"state":"water","y":150,"id":123},{"x":500,"state":"water","y":200,"id":124},{"x":500,"state":"water","y":250,"id":125},{"x":500,"state":"water","y":300,"id":126},{"x":500,"state":"water","y":350,"id":127},{"x":500,"state":"water","y":400,"id":128},{"x":500,"state":"water","y":450,"id":129},{"x":500,"state":"water","y":500,"id":130},{"x":500,"state":"water","y":550,"id":131},{"x":550,"state":"water","y":0,"id":132},{"x":550,"state":"water","y":50,"id":133},{"x":550,"state":"water","y":100,"id":134},{"x":550,"state":"water","y":150,"id":135},{"x":550,"state":"water","y":200,"id":136},{"x":550,"state":"water","y":250,"id":137},{"x":550,"state":"water","y":300,"id":138},{"x":550,"state":"water","y":350,"id":139},{"x":550,"state":"water","y":400,"id":140},{"x":550,"state":"water","y":450,"id":141},{"x":550,"state":"water","y":500,"id":142},{"x":550,"state":"water","y":550,"id":143}]'); var myCanvas = document.getElementById("mySea"); var myCtx = myCanvas.getContext("2d"); var enemyCanvas = document.getElementById("enemySea"); var enemyCtx = enemyCanvas.getContext("2d"); var mySquares = []; var gapSize = 3 var numRows = 12 var squareSize = getSquareSize() var ships = [2,5,4,3,3] var currentShipIndex = 0 var orientation = "vertical" /* Deze functie berekent hoe groot de vierkantjes van het grid moeten zijn om in de canvas te passen. */ function getSquareSize(){ var canvasWidth = myCanvas.width var optimalSquareSize = (canvasWidth / numRows) - gapSize return optimalSquareSize } /* Deze functie maakt een object aan waarin coordinaten, de staat, het schip en een aantal methoden zitten. De "draw" methode tekent de square op het canvas. De "isClicked" methode kijkt of de coordinaten die als parameter zijn meegegeven binnen de square vallen. De "getColor" methode kijkt welke kleur het vakje moet hebben. */ function createFriendlyGridSquare(x, y, gridNumber){ var square = { x: x, state: "water", y: y, getColor: mySeaColors, id: gridNumber, draw: drawSquare, isClicked: coordinatesOverlap } return square } /* In de gegeven "enemySquares" array zitten al de coordinaten waar de boten verstopt zijn. We moeten alleen nog de juiste methoden toevoegen. */ function insertEnemyGameMethods(gridNumber){ enemySquares[gridNumber].getColor = enemySeaColors enemySquares[gridNumber].draw = drawSquare enemySquares[gridNumber].isClicked = coordinatesOverlap return enemySquares[gridNumber] } /* Methode: die een vierkantje tekent op bepaalde coordinaten in een bepaalde canvas */ function drawSquare(context, state){ if(state != undefined){ this.state = state } context.beginPath(); context.fillStyle = this.getColor(); context.rect(this.x, this.y, squareSize, squareSize); context.fill(); } /* Methode: Als de coordinaten in de parameters overlappen met de coordinaten in het object is er overlap. */ function coordinatesOverlap(x,y){ var overlap = false if(x >= this.x && x <= this.x + squareSize && y >= this.y && y <= this.y + squareSize){ overlap = true } return overlap } /* Methode: In mijn zee zijn de boten geel */ function mySeaColors(){ if(this.state == "water"){ return "blue" }else if (this.state == "miss"){ return "red" }else if (this.state == "hit"){ return "green" }else if (this.state == "ship"){ return "yellow" } } /* Methode: In zijn zee zijn de boten verstopt (dezelfde kleur als de zee) */ function enemySeaColors(){ if(this.state == "water"){ return "blue" }else if (this.state == "miss"){ return "red" }else if (this.state == "hit"){ return "green" }else if (this.state == "ship"){ return "blue" } } /* De "e" parameter verwijst naar het click event waardoor deze functie (indirect) getriggered wordt. Deze "e" heeft een clientX en clientY veld waar de coordinaten van de klik in staan. Positie van de muis != positie van de elementen van de canvas. Daarvoor moeten we eerst de beginpunten van het canvas opvragen. Daar is de canvas.getBoundingClientRect() methode voor. Als je deze functie niet snapt is dat niet erg. */ function getMousePosRelativeToCanvas(e) { var rect = e.target.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; } /* Functie controleert of een boot geplaatst mag worden op een vierkantje. */ function isValidPosition(firstSquareIndex, currentShipSize){ var isValid = true; if(orientation == "vertical"){ //gaat niet over rand heen, % is modulo operator die naar de "restwaarde" van een deling kijkt. if(numRows - (firstSquareIndex % numRows) < currentShipSize ){ isValid = false } //gaat niet over ander schip heen for(var i = firstSquareIndex; i < firstSquareIndex + currentShipSize; i++){ if(mySquares[i] != undefined && mySquares[i].state == "ship"){ isValid = false } } }else if (orientation == "horziontal"){ //TODO: Controleer goed plaatsen bij horizontale orientatie. } return isValid } /* Tekent een boot op de plek waarop geklikt is, maar voert wel eerst wat controles uit. De currentship index wordt bij een succesvolle plaatsing opgehoogd, waardoor de volgende keer een nieuwe boot getekend wordt. */ function placeBoat(firstSquareIndex){ var currentShipSize = ships[currentShipIndex] if(isValidPosition(firstSquareIndex, currentShipSize)){ for(var i = firstSquareIndex ; i < firstSquareIndex + currentShipSize ; i++){ mySquares[i].draw(myCtx, "ship") } currentShipIndex += 1 } //TODO houd rekening met orientatie van de boot bij het plaatsen. } /* Tekent de canvas met onze zee, waar we vervolgens boten in kunnen plaatsen. */ function drawMySea(){ for (var x = 0, rowNumber = 0; rowNumber < numRows; x+=squareSize + gapSize , rowNumber++) { for (var y = 0, columnNumber=0; columnNumber < numRows; y+= squareSize + gapSize, columnNumber++) { var square = createFriendlyGridSquare(x, y, rowNumber*numRows + columnNumber) square.draw(myCtx, "water") mySquares.push(square) } } } /* Tekent de canvas van de vijand, zijn boten zijn onzichtbaar */ function drawEnemySea(){ for (var x = 0, rowNumber = 0; rowNumber < numRows; x+=squareSize + gapSize , rowNumber++) { for (var y = 0, columnNumber=0; columnNumber < numRows; y+= squareSize + gapSize, columnNumber++) { var square = insertEnemyGameMethods(rowNumber*numRows + columnNumber) square.draw(enemyCtx, undefined) // undefined want de boten zitten al in het object, we willen ze niet overschrijven. } } } /* Functie kijkt of de coordinaten waar geklikt is overeen komen met een plek op het grid. Als dat zo is, beslist de functie wat er moet gebeuren. */ function playHumanTurn(mouseX,mouseY){ for (var i = 0; i < enemySquares.length; i++) { if (enemySquares[i].isClicked(mouseX, mouseY)) { if(enemySquares[i].state == "water"){ enemySquares[i].draw(enemyCtx, "miss") }else if(enemySquares[i].state == "ship"){ enemySquares[i].draw(enemyCtx, "hit") } } } } /* De computer gooit een bom in het wilde weg ergens in jouw zee */ function playComputerTurn(){ var target = Math.floor(Math.random() * numRows * numRows); if(mySquares[target].state == "water"){ mySquares[target].draw(myCtx, "miss") }else if (mySquares[target].state == "ship"){ mySquares[target].draw(myCtx, "hit") } } /* De "clickEvent" parameter verwijst naar het "click event" waardoor deze functie getriggered wordt. Dit "event" is een object waar allerlei informatie over de click in verstopt zit. De functie probeert de huidige boot te plaatsen. Als er geen boot meer te plaatsen is, wordt de vijandige zee getekend, en worden de onclick methodes die nodig zijn voor het echte spel toegevoegd aan de juiste elementen. */ function gameSetupRound(clickEvent) { mouseX = parseInt(getMousePosRelativeToCanvas(clickEvent).x) mouseY = parseInt(getMousePosRelativeToCanvas(clickEvent).y) //controleer of de klik op een van de squares was. for (var i = 0; i < mySquares.length; i++) { if (mySquares[i].isClicked(mouseX, mouseY)) { //de draw methode van het vierkantje waarop geklikt is wordt uitgevoerd. placeBoat(i) console.log(mySquares[i].id) if(currentShipIndex == ships.length){ //als alle schepen geplaatst zijn, teken dan de vijandige zee drawEnemySea() //verander de onclick listener op het moment dat alle schepen geplaatst zijn. document.getElementById("enemySea").onclick = playRound; document.getElementById("mySea").onclick = ""; } } } } /* Dit is het echte spel, de functie wordt aangeroepen door een click event. de "clickEvent" parameter is het click event, daarin zit informatie over de coordinaten waar geklikt is. */ function playRound(clickEvent){ mouseX = parseInt(getMousePosRelativeToCanvas(clickEvent).x) mouseY = parseInt(getMousePosRelativeToCanvas(clickEvent).y) playHumanTurn(mouseX,mouseY) playComputerTurn() } drawMySea() document.getElementById("mySea").onclick = gameSetupRound; </script> </body> </html>