H5P.MemoryGame = (function (EventDispatcher, $) { // We don't want to go smaller than 100px per card(including the required margin) var CARD_MIN_SIZE = 100; // PX var CARD_STD_SIZE = 116; // PX var STD_FONT_SIZE = 16; // PX var LIST_PADDING = 1; // EMs /** * Memory Game Constructor * * @class H5P.MemoryGame * @extends H5P.EventDispatcher * @param {Object} parameters * @param {Number} id */ function MemoryGame(parameters, id) { /** @alias H5P.MemoryGame# */ var self = this; // Initialize event inheritance EventDispatcher.call(self); var flipped, timer, counter, popup, $feedback, $wrapper, maxWidth, numCols; var cards = []; var flipBacks = []; // Que of cards to be flipped back var numFlipped = 0; var removed = 0; /** * Check if these two cards belongs together. * * @private * @param {H5P.MemoryGame.Card} card * @param {H5P.MemoryGame.Card} mate * @param {H5P.MemoryGame.Card} correct */ var check = function (card, mate, correct) { if (mate !== correct) { // Incorrect, must be scheduled for flipping back flipBacks.push(card); flipBacks.push(mate); // Wait for next click to flip them back… if (numFlipped > 2) { // or do it straight away processFlipBacks(); } return; } // Remove them from the game. card.remove(); mate.remove(); // Update counters numFlipped -= 2; removed += 2; var isFinished = (removed === cards.length); var desc = card.getDescription(); if (desc !== undefined) { // Pause timer and show desciption. timer.pause(); var imgs = [card.getImage()]; if (card.hasTwoImages) { imgs.push(mate.getImage()); } popup.show(desc, imgs, cardStyles ? cardStyles.back : undefined, function () { if (isFinished) { // Game done finished(); } else { // Popup is closed, continue. timer.play(); } }); } else if (isFinished) { // Game done finished(); } }; /** * Game has finished! * @private */ var finished = function () { timer.stop(); $feedback.addClass('h5p-show'); // Create and trigger xAPI event 'completed' var completedEvent = self.createXAPIEventTemplate('completed'); completedEvent.setScoredResult(1, 1, self, true, true); completedEvent.data.statement.result.duration = 'PT' + (Math.round(timer.getTime() / 10) / 100) + 'S'; self.trigger(completedEvent); if (parameters.behaviour && parameters.behaviour.allowRetry) { // Create retry button var retryButton = createButton('reset', parameters.l10n.tryAgain || 'Try again?', function () { // Trigger handler (action) resetGame(); // Remove button from DOM $wrapper[0].removeChild(this); }); // Same size as cards retryButton.style.fontSize = $wrapper.children('ul')[0].style.fontSize; $wrapper[0].appendChild(retryButton); // Add to DOM } }; /** * Shuffle the cards and restart the game! * @private */ var resetGame = function () { // Reset cards removed = 0; for (var i = 0; i < cards.length; i++) { cards[i].reset(); } // Remove feedback $feedback[0].classList.remove('h5p-show'); // Reset timer and counter timer.reset(); counter.reset(); // Randomize cards H5P.shuffleArray(cards); setTimeout(function () { // Re-append to DOM after flipping back for (var i = 0; i < cards.length; i++) { cards[i].reAppend(); } // Scale new layout $wrapper.children('ul').children('.h5p-row-break').removeClass('h5p-row-break'); maxWidth = -1; self.trigger('resize'); }, 600); }; /** * Game has finished! * @private */ var createButton = function (name, label, action) { var buttonElement = document.createElement('div'); buttonElement.classList.add('h5p-memory-' + name); buttonElement.innerHTML = label; buttonElement.setAttribute('role', 'button'); buttonElement.tabIndex = 0; buttonElement.addEventListener('click', function (event) { action.apply(buttonElement); }, false); buttonElement.addEventListener('keypress', function (event) { if (event.which === 13 || event.which === 32) { // Enter or Space key event.preventDefault(); action.apply(buttonElement); } }, false); return buttonElement; }; /** * Adds card to card list and set up a flip listener. * * @private * @param {H5P.MemoryGame.Card} card * @param {H5P.MemoryGame.Card} mate */ var addCard = function (card, mate) { card.on('flip', function () { self.triggerXAPI('interacted'); // Keep track of time spent timer.play(); // Keep track of the number of flipped cards numFlipped++; if (flipped !== undefined) { var matie = flipped; // Reset the flipped card. flipped = undefined; setTimeout(function () { check(card, matie, mate); }, 800); } else { if (flipBacks.length > 1) { // Turn back any flipped cards processFlipBacks(); } // Keep track of the flipped card. flipped = card; } // Count number of cards turned counter.increment(); }); cards.push(card); }; /** * Will flip back two and two cards */ var processFlipBacks = function () { flipBacks[0].flipBack(); flipBacks[1].flipBack(); flipBacks.splice(0, 2); numFlipped -= 2; }; /** * @private */ var getCardsToUse = function () { var numCardsToUse = (parameters.behaviour && parameters.behaviour.numCardsToUse ? parseInt(parameters.behaviour.numCardsToUse) : 0); if (numCardsToUse <= 2 || numCardsToUse >= parameters.cards.length) { // Use all cards return parameters.cards; } // Pick random cards from pool var cardsToUse = []; var pickedCardsMap = {}; var numPicket = 0; while (numPicket < numCardsToUse) { var pickIndex = Math.floor(Math.random() * parameters.cards.length); if (pickedCardsMap[pickIndex]) { continue; // Already picked, try again! } cardsToUse.push(parameters.cards[pickIndex]); pickedCardsMap[pickIndex] = true; numPicket++; } return cardsToUse; }; var cardStyles, invertShades; if (parameters.lookNFeel) { // If the contrast between the chosen color and white is too low we invert the shades to create good contrast invertShades = (parameters.lookNFeel.themeColor && getContrast(parameters.lookNFeel.themeColor) < 1.7 ? -1 : 1); var backImage = (parameters.lookNFeel.cardBack ? H5P.getPath(parameters.lookNFeel.cardBack.path, id) : null); cardStyles = MemoryGame.Card.determineStyles(parameters.lookNFeel.themeColor, invertShades, backImage); } // Initialize cards. var cardsToUse = getCardsToUse(); for (var i = 0; i < cardsToUse.length; i++) { var cardParams = cardsToUse[i]; if (MemoryGame.Card.isValid(cardParams)) { // Create first card var cardTwo, cardOne = new MemoryGame.Card(cardParams.image, id, cardParams.description, cardStyles); if (MemoryGame.Card.hasTwoImages(cardParams)) { // Use matching image for card two cardTwo = new MemoryGame.Card(cardParams.match, id, cardParams.description, cardStyles); cardOne.hasTwoImages = cardTwo.hasTwoImages = true; } else { // Add two cards with the same image cardTwo = new MemoryGame.Card(cardParams.image, id, cardParams.description, cardStyles); } // Add cards to card list for shuffeling addCard(cardOne, cardTwo); addCard(cardTwo, cardOne); } } H5P.shuffleArray(cards); /** * Attach this game's html to the given container. * * @param {H5P.jQuery} $container */ self.attach = function ($container) { this.triggerXAPI('attempted'); // TODO: Only create on first attach! $wrapper = $container.addClass('h5p-memory-game').html(''); if (invertShades === -1) { $container.addClass('h5p-invert-shades'); } // Add cards to list var $list = $('