Week 5 — Memory Game
Build a complete memory / matching game: a deck of image cards, a shuffle, click hit-testing, match detection, a timed win condition and a flip-back delay. Then draw the same game with shapes instead of photos.
🎯 Objectives
- Model game pieces as objects (
Card) with data and adraw()method. - Build a deck from an array of pairs and store it in an array.
- Shuffle by repeatedly swapping two random entries.
- Load images with
new Image()and draw them withdrawImage. - Convert mouse coordinates and hit-test against card rectangles.
- Track turn state:
firstpick,firstcard,secondcard,matched. - Delay the flip-back with
setTimeout. - Time the game with
Dateand detect completion. - Draw regular polygons with paths and trigonometry (
memoryPolygons.html).
🛠 Weekly tasks
- Objective 1 — model game pieces as
Cardobjects with data and adraw()method: Activity 1 + buildmemoryPolygons.html. - Objective 2 — build a deck from an array of pairs and store it in an array: Activity 2 + build
memoryPolygons.html. - Objective 3 — shuffle by repeatedly swapping two random entries: Activity 3 + build
memoryPolygons.html. - Objective 4 — load images with
new Image()and draw them withdrawImage: Activity 4 + buildmemoryPictures.html. - Objective 5 — convert mouse coordinates and hit-test against card rectangles: Activity 5 + build
memoryPolygons.html. - Objective 6 — track turn state (
firstpick,firstcard,secondcard,matched): Activity 6 + buildmemoryPolygons.html. - Objective 7 — delay the flip-back with
setTimeout: Activity 7 + buildmemoryPictures.html. - Objective 8 — time the game with
Dateand detect completion: Activity 8 + buildmemoryPictures.html. - Objective 9 — draw regular polygons with paths and trigonometry: Activity 9 + build
triangle.htmlandmemoryPolygons.html. - Build your own
memoryPictures_yourname.html— using everything you have learned; add a “moves” counter next to the matches counter. (All objectives)
🎓 Lecture activity Open lecture slides →
A 90-minute session of 35 slides. Each concept is taught four ways: the big idea, a short example with its live output, a line-by-line explanation, and a 5-minute “try it” task. The lab work at 1:00 uses the Lab activity below.
Session plan
- 0:0010 minWarm-upReview questions on last week
- 0:1010 minGame & objectivesThis week’s game, objectives and key words
- 0:2035 minConceptsBig idea → code with live output → line by line → try it
- 0:555 minCheck understandingCommon mistakes and a 4-question quiz
- 1:0025 minLabBuild the files in the Lab activity below
- 1:255 minWrap-upBuild-your-own task, marking guide and recap
Concepts this week
- 1. Arrays of cards — A deck is an array of objects
- 2. Shuffling — Shuffle by swapping
- 3. Hit-tests & pauses — Was the click inside the card?
- 4. Polygons — Any polygon from one function
The slides open as a page on this site. Press F for full screen, the arrow keys to move, N for speaker notes, and P to save as PDF (turn on “Background graphics”).
🧪 Lab activity: work through the files Printable lab sheet
Create a folder named Week5 on your PC. Work through the files below
in order, creating each one and writing the code shown. Download the photo materials into the
folder for memoryPictures.html. Save each file and refresh the browser.
Want a head start? A scaffold file starter.html is provided — choose it in the
preview dropdown, copy it into your Week5 folder, and complete its TODOs.
-
Create your Week5 folder
On your own PC — do this once before starting.
- Create a folder named
Week5on your PC. - Download the photo materials into it (needed for
memoryPictures.html). - Open your
Week5folder in VS Code or Sublime Text.
- Create a folder named
-
triangle.html Objective 8
Week5\triangle.html — draw a triangle with paths.
- [Obj 8] Create
triangle.htmland add this scaffold code.<!DOCTYPE html> <html lang="en"> <head><title>Triangle</title></head> <body onLoad="init();"> <canvas id="canvas" width="400" height="400"> Your browser doesn't support the HTML5 element canvas. </canvas> <script> var rad = 50, centerX = 200, centerY = 200, n = 3; var angle = (2 * Math.PI) / n; function init() { ctx = document.getElementById("canvas").getContext("2d"); // TODO: draw the polygon with a path } </script> </body> </html> - [Obj 8] Add the polygon drawing with a path and trigonometry (inside
init()).ctx.fillStyle = "rgb(255,0,0)"; ctx.beginPath(); ctx.moveTo(centerX + rad * Math.cos(-0.5 * angle), centerY + rad * Math.sin(-0.5 * angle)); for (var i = 1; i < n; i++) { ctx.lineTo(centerX + rad * Math.cos((i - 0.5) * angle), centerY + rad * Math.sin((i - 0.5) * angle)); } ctx.closePath(); ctx.fill(); - Save and refresh.
- [Obj 8] Create
-
memoryPolygons.html Objectives 1, 2, 3, 5, 6, 7, 8, 9
Week5\memoryPolygons.html — the memory game with shapes.
- [Obj 1, 2] Create
memoryPolygons.htmland add this scaffold code.<!DOCTYPE html> <html lang="en"> <head> <title>Memory game using polygons</title> <style> form { width: 330px; margin: 20px; background-color: pink; padding: 20px; } input { text-align: right; } </style> </head> <body onLoad="init();"> <canvas id="canvas" width="900" height="400"> Your browser doesn't support the HTML5 element canvas. </canvas> <br /> Click on two cards to see if you have a match. <form name="f"> Number of matches: <input type="text" name="count" value="0" size="1" /> <p> Time taken to complete puzzle: <input type="text" name="elapsed" value=" " size="4" /> seconds. </form> <script> var ctx, canvas1, deck = [], firstpick = true, firstcard, secondcard, matched; var cardrad = 30, cardwidth = 4 * cardrad, cardheight = 4 * cardrad; var firstsx = 30, firstsy = 50, margin = 30, starttime; // TODO: add the Card constructor and makedeck() // TODO: add shuffle(), choose() and flipback() function init() { ctx = document.getElementById("canvas").getContext("2d"); canvas1 = document.getElementById("canvas"); // TODO: add the click listener, make and shuffle the deck } </script> </body> </html> - [Obj 1, 2] Add the
Cardconstructor, thenmakedeck()to build the deck. Each pair shares the sameinfonumber (3–8), and every card is drawn as it is created.function Card(sx, sy, swidth, sheight, info) { this.sx = sx; this.sy = sy; this.swidth = swidth; this.sheight = sheight; this.info = info; this.draw = drawback; } function makedeck() { var cx = firstsx, cy = firstsy; for (var i = 3; i < 9; i++) { var acard = new Card(cx, cy, cardwidth, cardheight, i); deck.push(acard); var bcard = new Card(cx, cy + cardheight + margin, cardwidth, cardheight, i); deck.push(bcard); cx = cx + cardwidth + margin; acard.draw(); bcard.draw(); } } - [Obj 3] Add
shuffle(): swap theinfoof two random cards, over and over, so the pairs land in random positions.function shuffle() { var dl = deck.length; for (var nt = 0; nt < 3 * dl; nt++) { var i = Math.floor(Math.random() * dl); var k = Math.floor(Math.random() * dl); var holder = deck[i].info; deck[i].info = deck[k].info; deck[k].info = holder; } } - [Obj 1, 2, 9] Add the colours and the drawing functions:
drawback()paints the hidden back, whilePolycard/drawpoly()draw the polygon on the front.var backcolor = "rgb(128,0,128)"; var frontbgcolor = "rgb(251,215,73)"; var polycolor = "rgb(254,11,0)"; var tablecolor = "rgb(255,255,255)"; function drawback() { ctx.fillStyle = backcolor; ctx.fillRect(this.sx, this.sy, this.swidth, this.sheight); } function Polycard(sx, sy, rad, n) { this.sx = sx; this.sy = sy; this.rad = rad; this.n = n; this.angle = (2 * Math.PI) / n; this.draw = drawpoly; } function drawpoly() { ctx.fillStyle = frontbgcolor; ctx.fillRect(this.sx - 2 * this.rad, this.sy - 2 * this.rad, 4 * this.rad, 4 * this.rad); ctx.beginPath(); ctx.fillStyle = polycolor; var rad = this.rad; ctx.moveTo(this.sx + rad * Math.cos(-0.5 * this.angle), this.sy + rad * Math.sin(-0.5 * this.angle)); for (var i = 1; i < this.n; i++) { ctx.lineTo(this.sx + rad * Math.cos((i - 0.5) * this.angle), this.sy + rad * Math.sin((i - 0.5) * this.angle)); } ctx.fill(); } - [Obj 5, 6, 8] Add
choose(): it hit-tests the click, shows the polygon on the first and second pick, counts a match, and — once every pair is found — shows the time taken.function choose(ev) { var mx = ev.pageX, my = ev.pageY; var card; for (var i = 0; i < deck.length; i++) { card = deck[i]; if (card.sx >= 0 && mx > card.sx && mx < card.sx + card.swidth && my > card.sy && my < card.sy + card.sheight) { break; } } if (i < deck.length) { if (firstpick) { firstcard = i; firstpick = false; new Polycard(card.sx + cardwidth * 0.5, card.sy + cardheight * 0.5, cardrad, card.info).draw(); } else { secondcard = i; new Polycard(card.sx + cardwidth * 0.5, card.sy + cardheight * 0.5, cardrad, card.info).draw(); matched = (card.info == deck[firstcard].info); if (matched) { var nm = 1 + Number(document.f.count.value); document.f.count.value = String(nm); if (nm >= 0.5 * deck.length) { var now = new Date(); var nt = Number(now.getTime()); var seconds = Math.floor(0.5 + (nt - starttime) / 1000); document.f.elapsed.value = String(seconds); } } firstpick = true; setTimeout(flipback, 1000); } } } - [Obj 6, 7] Add
flipback(): non-matching cards are drawn again; matched cards are erased and moved off-screen (sx = -1) so they can no longer be clicked.function flipback() { if (!matched) { deck[firstcard].draw(); deck[secondcard].draw(); } else { ctx.fillStyle = tablecolor; ctx.fillRect(deck[secondcard].sx, deck[secondcard].sy, deck[secondcard].swidth, deck[secondcard].sheight); ctx.fillRect(deck[firstcard].sx, deck[firstcard].sy, deck[firstcard].swidth, deck[firstcard].sheight); deck[secondcard].sx = -1; deck[firstcard].sx = -1; } } - [Obj 1, 2, 3, 8] Fill in
init(), which runs fromonLoad: get the context, build and shuffle the deck, and record the start time.function init() { ctx = document.getElementById("canvas").getContext("2d"); canvas1 = document.getElementById("canvas"); canvas1.addEventListener("click", choose, false); makedeck(); document.f.count.value = "0"; document.f.elapsed.value = ""; starttime = new Date(); starttime = Number(starttime.getTime()); shuffle(); } - Save and refresh, then play.
- [Obj 1, 2] Create
-
memoryPictures.html Objectives 4, 7
Week5\memoryPictures.html — the memory game with photos.
- [Obj 1, 2] Create
memoryPictures.htmland add this scaffold code (your memory game, ready for images).<!DOCTYPE html> <html lang="en"> <head><title>Memory game using pictures</title></head> <body onLoad="init();"> <canvas id="canvas" width="900" height="400"> Your browser doesn't support the HTML5 element canvas. </canvas> <script> var ctx, canvas1, deck = [], firstpick = true, firstcard = -1, secondcard, matched; var cardwidth = 100, cardheight = 100, firstsx = 30, firstsy = 50, margin = 30; var starttime, count = 0, finished = false; var pairs = [ // TODO: add pairs of image file names ]; // TODO: add the Card constructor and makedeck() that loads the images // TODO: add shuffle(), choose() and flipback() function init() { ctx = document.getElementById("canvas").getContext("2d"); canvas1 = document.getElementById("canvas"); // TODO: add the click listener, make and shuffle the deck } </script> </body> </html> - [Obj 2, 4] Add the pairs of image files — two photos per pair, and the pair number becomes the card's
info.var pairs = [ ["anneGorge.jpg", "anneNow.jpg"], ["esther.jpg", "pigtailEsther.jpg"], ["pigtailJeanine.jpg", "jeanineGorge.jpg"], ["pigtailAviva.jpg", "avivaCuba.jpg"], ["pigtailAnnika.jpg", "annikaTooth.jpg"] ]; - [Obj 4] Load each image with
new Image().var pica = new Image(); pica.src = pairs[i][0]; - [Obj 4] Draw the image when a card is revealed.
ctx.drawImage(card.img, card.sx, card.sy, card.swidth, card.sheight); - [Obj 7] Time the game with
Date.starttime = new Date(); var seconds = Math.floor(0.5 + (now - starttime) / 1000); - [Obj 1, 2, 4] Add the
Cardconstructor, deck and drawing/game functions.var backcolor = "rgb(128,0,128)"; var tablecolor = "rgb(255,255,255)"; function Card(sx, sy, swidth, sheight, img, info) { this.sx = sx; this.sy = sy; this.swidth = swidth; this.sheight = sheight; this.img = img; this.info = info; this.draw = drawback; } function drawback() { ctx.fillStyle = backcolor; ctx.fillRect(this.sx, this.sy, this.swidth, this.sheight); } function makedeck() { var cx = firstsx, cy = firstsy; for (var i = 0; i < pairs.length; i++) { var pica = new Image(); pica.src = pairs[i][0]; var picb = new Image(); picb.src = pairs[i][1]; deck.push(new Card(cx, cy, cardwidth, cardheight, pica, i)); deck.push(new Card(cx, cy + cardheight + margin, cardwidth, cardheight, picb, i)); cx = cx + cardwidth + margin; deck[deck.length - 2].draw(); deck[deck.length - 1].draw(); } } function shuffle() { for (var nt = 0; nt < 3 * deck.length; nt++) { var i = Math.floor(Math.random() * deck.length), k = Math.floor(Math.random() * deck.length); var hi = deck[i].info, him = deck[i].img; deck[i].info = deck[k].info; deck[i].img = deck[k].img; deck[k].info = hi; deck[k].img = him; } } function choose(ev) { var mx = ev.pageX, my = ev.pageY, card; for (var i = 0; i < deck.length; i++) { card = deck[i]; if (card.sx >= 0) // the card is still in play if (mx > card.sx && mx < card.sx + card.swidth && my > card.sy && my < card.sy + card.sheight) { if (firstpick || i != firstcard) { break; } // first pick, or a different second card } } if (i < deck.length) { if (firstpick) { firstcard = i; firstpick = false; ctx.drawImage(card.img, card.sx, card.sy, card.swidth, card.sheight); } else { secondcard = i; ctx.drawImage(card.img, card.sx, card.sy, card.swidth, card.sheight); if ((card.info == deck[firstcard].info) && (firstcard != secondcard)) { matched = true; count++; ctx.fillStyle = tablecolor; ctx.fillRect(10, 340, 900, 100); ctx.fillStyle = backcolor; ctx.fillText("Number of matches so far: " + String(count), 10, 360); if (count >= 0.5 * deck.length) { finished = true; var seconds = Math.floor(0.5 + (Date.now() - starttime) / 1000); ctx.fillStyle = tablecolor; ctx.fillRect(0, 0, 900, 400); ctx.fillStyle = backcolor; ctx.fillText("You finished in " + String(seconds) + " secs.", 10, 100); ctx.fillText("Reload the page to try again.", 10, 300); } } else { matched = false; } firstpick = true; setTimeout(flipback, 1000); } } } function flipback() { if (finished) { return; } // the game is over; keep the final message on screen if (!matched) { deck[firstcard].draw(); deck[secondcard].draw(); } else { ctx.fillStyle = tablecolor; ctx.fillRect(deck[secondcard].sx, deck[secondcard].sy, deck[secondcard].swidth, deck[secondcard].sheight); ctx.fillRect(deck[firstcard].sx, deck[firstcard].sy, deck[firstcard].swidth, deck[firstcard].sheight); deck[secondcard].sx = -1; deck[firstcard].sx = -1; } } - [Obj 1, 2, 3] Add
init()to start the game.ctx = document.getElementById("canvas").getContext("2d"); canvas1 = document.getElementById("canvas"); canvas1.addEventListener("click", choose, false); makedeck(); shuffle(); ctx.font = "bold 20pt sans-serif"; ctx.fillText("Click on two cards to make a match.", 10, 20); ctx.fillText("Number of matches so far: 0", 10, 360); starttime = new Date(); starttime = Number(starttime.getTime()); - Save and refresh, then finish a full game.
- [Obj 1, 2] Create
📝 Tasks for this file:
Cards are hidden by drawing a coloured rectangle over them. When a match is found the card
positions are set to -1 so they can no longer be clicked.
📚 Game flow
init()sets up the context, builds the deck and shuffles it.- Each
clickrunschoose(), which hit-tests the mouse position. - The first pick is revealed; the second pick is compared by
info(the pair number). setTimeout(flipback, 1000)hides non-matching cards again after one second.- When every pair is matched the elapsed time is displayed.