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 a draw() 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 with drawImage.
  • 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 Date and detect completion.
  • Draw regular polygons with paths and trigonometry (memoryPolygons.html).

🛠 Weekly tasks

  1. Objective 1 — model game pieces as Card objects with data and a draw() method: Activity 1 + build memoryPolygons.html.
  2. Objective 2 — build a deck from an array of pairs and store it in an array: Activity 2 + build memoryPolygons.html.
  3. Objective 3 — shuffle by repeatedly swapping two random entries: Activity 3 + build memoryPolygons.html.
  4. Objective 4 — load images with new Image() and draw them with drawImage: Activity 4 + build memoryPictures.html.
  5. Objective 5 — convert mouse coordinates and hit-test against card rectangles: Activity 5 + build memoryPolygons.html.
  6. Objective 6 — track turn state (firstpick, firstcard, secondcard, matched): Activity 6 + build memoryPolygons.html.
  7. Objective 7 — delay the flip-back with setTimeout: Activity 7 + build memoryPictures.html.
  8. Objective 8 — time the game with Date and detect completion: Activity 8 + build memoryPictures.html.
  9. Objective 9 — draw regular polygons with paths and trigonometry: Activity 9 + build triangle.html and memoryPolygons.html.
  10. 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

  1. 0:0010 minWarm-upReview questions on last week
  2. 0:1010 minGame & objectivesThis week’s game, objectives and key words
  3. 0:2035 minConceptsBig idea → code with live output → line by line → try it
  4. 0:555 minCheck understandingCommon mistakes and a 4-question quiz
  5. 1:0025 minLabBuild the files in the Lab activity below
  6. 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.

  1. Create your Week5 folder

    On your own PC — do this once before starting.

    1. Create a folder named Week5 on your PC.
    2. Download the photo materials into it (needed for memoryPictures.html).
    3. Open your Week5 folder in VS Code or Sublime Text.
  2. triangle.html Objective 8

    Week5\triangle.html — draw a triangle with paths.

    1. [Obj 8] Create triangle.html and 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>
    2. [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();
    3. Save and refresh.
  3. memoryPolygons.html Objectives 1, 2, 3, 5, 6, 7, 8, 9

    Week5\memoryPolygons.html — the memory game with shapes.

    1. [Obj 1, 2] Create memoryPolygons.html and 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>
    2. [Obj 1, 2] Add the Card constructor, then makedeck() to build the deck. Each pair shares the same info number (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();
        }
      }
    3. [Obj 3] Add shuffle(): swap the info of 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;
        }
      }
    4. [Obj 1, 2, 9] Add the colours and the drawing functions: drawback() paints the hidden back, while Polycard / 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();
      }
    5. [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);
          }
        }
      }
    6. [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;
        }
      }
    7. [Obj 1, 2, 3, 8] Fill in init(), which runs from onLoad: 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();
      }
    8. Save and refresh, then play.
  4. memoryPictures.html Objectives 4, 7

    Week5\memoryPictures.html — the memory game with photos.

    1. [Obj 1, 2] Create memoryPictures.html and 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>
    2. [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"]
      ];
    3. [Obj 4] Load each image with new Image().
      var pica = new Image();
      pica.src = pairs[i][0];
    4. [Obj 4] Draw the image when a card is revealed.
      ctx.drawImage(card.img, card.sx, card.sy, card.swidth, card.sheight);
    5. [Obj 7] Time the game with Date.
      starttime = new Date();
      var seconds = Math.floor(0.5 + (now - starttime) / 1000);
    6. [Obj 1, 2, 4] Add the Card constructor, 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;
        }
      }
    7. [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());
    8. Save and refresh, then finish a full game.
Finished example output

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

  1. init() sets up the context, builds the deck and shuffles it.
  2. Each click runs choose(), which hit-tests the mouse position.
  3. The first pick is revealed; the second pick is compared by info (the pair number).
  4. setTimeout(flipback, 1000) hides non-matching cards again after one second.
  5. When every pair is matched the elapsed time is displayed.