diff --git a/card.js b/card.js index 58bfcfb..9b1aa90 100644 --- a/card.js +++ b/card.js @@ -4,11 +4,13 @@ * Controls all the operations for each card. * * @class H5P.MemoryGame.Card + * @extends H5P.EventDispatcher * @param {Object} image * @param {number} id * @param {string} [description] */ MemoryGame.Card = function (image, id, description) { + /** @alias H5P.MemoryGame.Card# */ var self = this; // Initialize event inheritance @@ -53,6 +55,13 @@ $card.addClass('h5p-matched'); }; + /** + * Reset card to natural state + */ + self.reset = function () { + $card[0].classList.remove('h5p-flipped', 'h5p-matched'); + }; + /** * Get card description. * @@ -91,7 +100,15 @@ self.flip(); }) .end(); - }; + }; + + /** + * Re-append to parent container + */ + self.reAppend = function () { + var parent = $card[0].parentElement.parentElement; + parent.appendChild($card[0].parentElement); + }; }; // Extends the event dispatcher diff --git a/counter.js b/counter.js index 39343c8..d0e6c75 100644 --- a/counter.js +++ b/counter.js @@ -7,16 +7,32 @@ * @param {H5P.jQuery} $container */ MemoryGame.Counter = function ($container) { + /** @alias H5P.MemoryGame.Counter# */ var self = this; var current = 0; + /** + * @private + */ + var update = function () { + $container[0].innerText = current; + }; + /** * Increment the counter. */ self.increment = function () { current++; - $container.text(current); + update(); + }; + + /** + * Revert counter back to its natural state + */ + self.reset = function () { + current = 0; + update(); }; }; diff --git a/language/ar.json b/language/ar.json index 6b999ef..3ef63b3 100644 --- a/language/ar.json +++ b/language/ar.json @@ -29,8 +29,10 @@ "description": "These options will let you control how the game behaves.", "fields": [ { - "englishLabel": "Put the cards in a grid layout", - "label": "Put the cards in a grid layout" + "englishLabel": "Position the cards in a square", + "label": "Position the cards in a square", + "englishDescription": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container.", + "description": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container." }, { "englishLabel": "Number of cards to use", @@ -55,6 +57,12 @@ }, { "label": "نص الملاحظات" + }, + { + "englishLabel": "Try again button text", + "label": "Try again button text", + "englishDefault": "Try again?", + "default": "Try again?" } ] } diff --git a/language/de.json b/language/de.json index e84760f..d776e7c 100644 --- a/language/de.json +++ b/language/de.json @@ -29,8 +29,10 @@ "description": "These options will let you control how the game behaves.", "fields": [ { - "englishLabel": "Put the cards in a grid layout", - "label": "Put the cards in a grid layout" + "englishLabel": "Position the cards in a square", + "label": "Position the cards in a square", + "englishDescription": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container.", + "description": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container." }, { "englishLabel": "Number of cards to use", @@ -58,6 +60,12 @@ { "label": "Text als Rückmeldung", "default": "Gute Arbeit!" + }, + { + "englishLabel": "Try again button text", + "label": "Try again button text", + "englishDefault": "Try again?", + "default": "Try again?" } ] } diff --git a/language/fr.json b/language/fr.json index aa3b53b..41f9217 100644 --- a/language/fr.json +++ b/language/fr.json @@ -29,8 +29,10 @@ "description": "These options will let you control how the game behaves.", "fields": [ { - "englishLabel": "Put the cards in a grid layout", - "label": "Put the cards in a grid layout" + "englishLabel": "Position the cards in a square", + "label": "Position the cards in a square", + "englishDescription": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container.", + "description": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container." }, { "englishLabel": "Number of cards to use", @@ -58,6 +60,12 @@ { "label": "Texte de l'appréciation finale", "default": "Bien joué !" + }, + { + "englishLabel": "Try again button text", + "label": "Try again button text", + "englishDefault": "Try again?", + "default": "Try again?" } ] } diff --git a/language/it.json b/language/it.json index 5b58c96..697442b 100644 --- a/language/it.json +++ b/language/it.json @@ -29,8 +29,10 @@ "description": "These options will let you control how the game behaves.", "fields": [ { - "englishLabel": "Put the cards in a grid layout", - "label": "Put the cards in a grid layout" + "englishLabel": "Position the cards in a square", + "label": "Position the cards in a square", + "englishDescription": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container.", + "description": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container." }, { "englishLabel": "Number of cards to use", @@ -55,6 +57,12 @@ }, { "label": "Testo Feedback" + }, + { + "englishLabel": "Try again button text", + "label": "Try again button text", + "englishDefault": "Try again?", + "default": "Try again?" } ] } diff --git a/language/nb.json b/language/nb.json index b90dd68..5633b0d 100644 --- a/language/nb.json +++ b/language/nb.json @@ -35,8 +35,10 @@ "description": "Disse instillingene lar deg bestemme hvordan spillet skal oppføre seg.", "fields": [ { - "englishLabel": "Put the cards in a grid layout", - "label": "Putt kortene i et rutenett" + "englishLabel": "Position the cards in a square", + "label": "Plasser kortene i en firkant", + "englishDescription": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container.", + "description": "Vil forsøk å samsvare antall kolonner og rader når kortene legges ut. Etterpå vil kortene bli skalert til å passe beholderen." }, { "englishLabel": "Number of cards to use", @@ -71,6 +73,12 @@ "label": "Tilbakemeldingstekst", "englishDefault": "Good work!", "default": "Godt jobbet!" + }, + { + "englishLabel": "Try again button text", + "label": "Prøv på nytt-tekst", + "englishDefault": "Try again?", + "default": "Prøv på nytt?" } ] } diff --git a/library.json b/library.json index 411aab1..79cb421 100644 --- a/library.json +++ b/library.json @@ -3,9 +3,9 @@ "description": "See how many cards you can remember!", "majorVersion": 1, "minorVersion": 2, - "patchVersion": 1, + "patchVersion": 2, "runnable": 1, - "author": "Amendor AS", + "author": "Joubel AS", "license": "MIT", "machineName": "H5P.MemoryGame", "preloadedCss": [ @@ -25,6 +25,9 @@ }, { "path": "popup.js" + }, + { + "path": "timer.js" } ], "preloadedDependencies": [ @@ -34,4 +37,4 @@ "minorVersion": 4 } ] -} \ No newline at end of file +} diff --git a/memory-game.css b/memory-game.css index f491184..cb3f9ef 100644 --- a/memory-game.css +++ b/memory-game.css @@ -172,6 +172,7 @@ background: #fff; padding: 1em; width: 20em; + max-width: 90%; position: absolute; top: 50%; left: 50%; @@ -182,7 +183,6 @@ } .h5p-memory-game .h5p-memory-image { float: left; - margin: 0 1em 1em 0; border: 2px solid #d0d0d0; box-sizing: border-box; -moz-box-sizing: border-box; @@ -190,7 +190,25 @@ background: #f0f0f0; width: 6.25em; height: 6.25em; + text-align: center; } .h5p-memory-game .h5p-row-break { clear: left; } +.h5p-memory-game .h5p-memory-desc { + margin-left: 7em; +} +.h5p-memory-reset { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + cursor: pointer; + font-style: italic; + text-shadow: 0 0 0.5em white; + padding: 0.125em 0.25em; + line-height: 1; +} +.h5p-memory-reset:focus { + outline: dashed pink; +} diff --git a/memory-game.js b/memory-game.js index 5165c1c..2e2cd2f 100644 --- a/memory-game.js +++ b/memory-game.js @@ -9,11 +9,13 @@ H5P.MemoryGame = (function (EventDispatcher, $) { /** * Memory Game Constructor * - * @class + * @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 @@ -21,6 +23,8 @@ H5P.MemoryGame = (function (EventDispatcher, $) { 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; /** @@ -32,47 +36,134 @@ H5P.MemoryGame = (function (EventDispatcher, $) { * @param {H5P.MemoryGame.Card} correct */ var check = function (card, mate, correct) { - if (mate === correct) { - // Remove them from the game. - card.remove(); - mate.remove(); + if (mate !== correct) { + // Incorrect, must be scheduled for flipping back + flipBacks.push(card); + flipBacks.push(mate); - removed += 2; - - var finished = (removed === cards.length); - var desc = card.getDescription(); - - if (finished) { - self.triggerXAPIScored(1, 1, 'completed'); - } - - if (desc !== undefined) { - // Pause timer and show desciption. - timer.pause(); - popup.show(desc, card.getImage(), function () { - if (finished) { - // Game has finished - $feedback.addClass('h5p-show'); - if (parameters.behaviour && parameters.behaviour.allowRetry) { /* TODO */ } - } - else { - // Popup is closed, continue. - timer.play(); - } - }); - } - else if (finished) { - // Game has finished - timer.stop(); - $feedback.addClass('h5p-show'); - if (parameters.behaviour && parameters.behaviour.allowRetry) { /* TODO */ } + // Wait for next click to flip them back… + if (numFlipped > 2) { + // or do it straight away + processFlipBacks(); } + return; } - else { - // Flip them back - card.flipBack(); - mate.flipBack(); + + // 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 (isFinished) { + self.triggerXAPIScored(1, 1, 'completed'); } + + if (desc !== undefined) { + // Pause timer and show desciption. + timer.pause(); + popup.show(desc, card.getImage(), 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'); + 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; }; /** @@ -88,6 +179,9 @@ H5P.MemoryGame = (function (EventDispatcher, $) { // 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. @@ -98,6 +192,11 @@ H5P.MemoryGame = (function (EventDispatcher, $) { }, 800); } else { + if (flipBacks.length > 1) { + // Turn back any flipped cards + processFlipBacks(); + } + // Keep track of the flipped card. flipped = card; } @@ -109,6 +208,16 @@ H5P.MemoryGame = (function (EventDispatcher, $) { 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 */ @@ -191,17 +300,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) { '
0
' + '').appendTo($container); - timer = new H5P.Timer(100); - timer.notify('every_tenth_second', function () { - var time = timer.getTime(); - var minutes = H5P.Timer.extractTimeElement(time, 'minutes'); - var seconds = H5P.Timer.extractTimeElement(time, 'seconds') % 60; - if (seconds < 10) { - seconds = '0' + seconds; - } - $status.find('.h5p-time-spent').text(minutes + ':' + seconds); - }); - + timer = new MemoryGame.Timer($status.find('.h5p-time-spent')[0]); counter = new MemoryGame.Counter($status.find('.h5p-card-turns')); popup = new MemoryGame.Popup($container); @@ -268,6 +367,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) { // We use font size to evenly scale all parts of the cards. $list.css('font-size', fontSize + 'px'); + popup.setSize(fontSize); // due to rounding errors in browsers the margins may vary a bit… }; diff --git a/popup.js b/popup.js index 44a12de..bee3ceb 100644 --- a/popup.js +++ b/popup.js @@ -7,6 +7,7 @@ * @param {H5P.jQuery} $container */ MemoryGame.Popup = function ($container) { + /** @alias H5P.MemoryGame.Popup# */ var self = this; var closed; @@ -39,6 +40,23 @@ closed = undefined; } }; + + /** + * Sets popup size relative to the card size + * @param {number} fontSize + */ + self.setSize = function (fontSize) { + // Set image size + $image[0].style.fontSize = fontSize + 'px'; + + // Determine card size + var cardSize = fontSize * 6.25; // From CSS + + // Set popup size + $popup[0].style.minWidth = (cardSize * 2) + 'px'; + $popup[0].style.minHeight = cardSize + 'px'; + $desc[0].style.marginLeft = (cardSize + 20) + 'px'; + }; }; })(H5P.MemoryGame, H5P.jQuery); diff --git a/semantics.json b/semantics.json index 4faf9bf..b25e38c 100644 --- a/semantics.json +++ b/semantics.json @@ -52,7 +52,8 @@ { "name": "useGrid", "type": "boolean", - "label": "Put the cards in a grid layout", + "label": "Position the cards in a square", + "description": "Will try to match the number of columns and rows when laying out the cards. Afterward, the cards will be scaled to fit the container.", "importance": "low", "default": true }, @@ -101,6 +102,13 @@ "name": "feedback", "type": "text", "default": "Good work!" + }, + { + "label": "Try again button text", + "importance": "low", + "name": "tryAgain", + "type": "text", + "default": "Try again?" } ] } diff --git a/timer.js b/timer.js new file mode 100644 index 0000000..339392e --- /dev/null +++ b/timer.js @@ -0,0 +1,50 @@ +(function (MemoryGame, Timer) { + + /** + * Adapter between memory game and H5P.Timer + * + * @class H5P.MemoryGame.Timer + * @extends H5P.Timer + * @param {Element} element + */ + MemoryGame.Timer = function (element) { + /** @alias H5P.MemoryGame.Timer# */ + var self = this; + + // Initialize event inheritance + Timer.call(self, 100); + + /** @private {string} */ + var naturalState = element.innerText; + + /** + * Set up callback for time updates. + * Formats time stamp for humans. + * + * @private + */ + var update = function () { + var time = self.getTime(); + + var minutes = Timer.extractTimeElement(time, 'minutes'); + var seconds = Timer.extractTimeElement(time, 'seconds') % 60; + if (seconds < 10) { + seconds = '0' + seconds; + } + + element.innerText = minutes + ':' + seconds; + }; + + // Setup default behavior + self.notify('every_tenth_second', update); + self.on('reset', function () { + element.innerText = naturalState; + self.notify('every_tenth_second', update); + }); + }; + + // Inheritance + MemoryGame.Timer.prototype = Object.create(Timer.prototype); + MemoryGame.Timer.prototype.constructor = MemoryGame.Timer; + +})(H5P.MemoryGame, H5P.Timer);