├── img └── geometry2.png ├── docs └── Git - Team Workflow.png ├── .gitattributes ├── webpack.config.js ├── CHANGELOG.md ├── package.json ├── js ├── app.js ├── Deck.js ├── GamePlay.js └── GameUI.js ├── .gitignore ├── index.html ├── COLLABORATOR_GUIDE.md ├── css └── app.css ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md └── dist └── bundle-app.js /img/geometry2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdmedlock/memorygame/HEAD/img/geometry2.png -------------------------------------------------------------------------------- /docs/Git - Team Workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdmedlock/memorygame/HEAD/docs/Git - Team Workflow.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | * text=auto 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "production", 3 | entry: { 4 | app: ["./js/app.js"] 5 | }, 6 | output: { 7 | filename: 'bundle-app.js' 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### 1.0.2 - 2018-06-25 4 | 5 | - Correct game freeze following double click on card 6 | 7 | ### 1.0.1 - 2018-06-24 8 | 9 | - Correct multiple event handler problems 10 | - Discard new card click events until the preceeding pair have been evaluated 11 | - Ignore clicks outside the boundaries of a card 12 | - Prevent cards from being stuck in the revealed state 13 | - Ignore clicks on previously matched cards 14 | - Change the player rating to start with 3 instead of 0 stars 15 | - Update readme.md to document module/library dependencies 16 | 17 | ### 1.0.0 - 2018-06-24 18 | 19 | - Initial release 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memorygame", 3 | "version": "1.0.0", 4 | "description": "Memory Game assignment for Udacity Front-End Web Developer Nanodegree", 5 | "main": "app.js", 6 | "directories": { 7 | "doc": "docs" 8 | }, 9 | "scripts": { 10 | "build": "webpack --config webpack.config.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jdmedlock/memorygame.git" 15 | }, 16 | "author": "Jim D. Medlock", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/jdmedlock/memorygame/issues" 20 | }, 21 | "homepage": "https://github.com/jdmedlock/memorygame#readme", 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "webpack": "^4.12.0", 25 | "webpack-cli": "^3.0.8" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | import Deck from './Deck'; 2 | import GamePlay from './GamePlay'; 3 | import GameUI from './GameUI'; 4 | 5 | // Instantiate the classes that implement the games functionality. 6 | const deck = new Deck(); 7 | const gamePlay = new GamePlay(); 8 | const gameUI = new GameUI(); 9 | 10 | gamePlay.setDeck(deck); 11 | gamePlay.setGameUI(gameUI); 12 | gamePlay.startNewGame(); 13 | 14 | // Define event handlers for each UI element to start the game 15 | const deckElement = document.querySelector('.deck'); 16 | document.querySelector('.deck').addEventListener('click', (event) => { 17 | gamePlay.turn(event.target.getAttribute('id')); 18 | }); 19 | 20 | const restartButton = document.querySelector('.restart'); 21 | restartButton.addEventListener('click', (event) => { 22 | gamePlay.startNewGame(); 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Matching Game 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Matching Game

18 |
19 | 20 |
21 | 32 | 33 | 34 | 00 35 | : 36 | 00 37 | 38 | 39 |
40 | 0 Moves 41 |
42 | 43 |
44 | 45 |
46 |
47 | 48 | 50 |
51 | 52 |
53 |
54 |
55 | 56 |

You are a Winner!

57 |

You won in 58 | 00 minutes, 59 | 00 seconds, using 60 | 00 moves, for 61 | 0 stars 62 |

63 | 64 | Play another game! 65 | 66 |
67 |
68 |
69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /js/Deck.js: -------------------------------------------------------------------------------- 1 | 2 | class Deck { 3 | /** 4 | * @description Creates an instance of the Deck class. 5 | * @memberof Deck 6 | */ 7 | constructor() { 8 | /* 9 | * Define the card decks cardDeck is the template deck defining the 10 | * attributes of each card, while gameDeck is the shuffled copy of 11 | * created at the start of each game. 12 | * 13 | * symbol - FontAwesome icon name 14 | * faceup - true if the card is faceup; false if facedown 15 | * matched - true if the card has been sucessfully matched; false if it 16 | * remains unmatched 17 | */ 18 | this.templateCardDeck = [ 19 | {symbol: 'fa-diamond', faceup: false, matched: false}, 20 | {symbol: 'fa-diamond', faceup: false, matched: false}, 21 | {symbol: 'fa-paper-plane-o', faceup: false, matched: false}, 22 | {symbol: 'fa-paper-plane-o', faceup: false, matched: false}, 23 | {symbol: 'fa-anchor', faceup: false, matched: false}, 24 | {symbol: 'fa-anchor', faceup: false, matched: false}, 25 | {symbol: 'fa-bolt', faceup: false, matched: false}, 26 | {symbol: 'fa-bolt', faceup: false, matched: false}, 27 | {symbol: 'fa-cube', faceup: false, matched: false}, 28 | {symbol: 'fa-cube', faceup: false, matched: false}, 29 | {symbol: 'fa-leaf', faceup: false, matched: false}, 30 | {symbol: 'fa-leaf', faceup: false, matched: false}, 31 | {symbol: 'fa-bicycle', faceup: false, matched: false}, 32 | {symbol: 'fa-bicycle', faceup: false, matched: false}, 33 | {symbol: 'fa-bomb', faceup: false, matched: false}, 34 | {symbol: 'fa-bomb', faceup: false, matched: false}, 35 | ]; 36 | } 37 | 38 | /** 39 | * @description Check to see if two cards have matching symbols 40 | * @param {Object[]} cardDeck Array of card objects used in the game 41 | * @param {Number} firstCardIndex Index of the first card to compare 42 | * @param {Number} secondCardIndex Index of the second card to compare 43 | * @returns {Boolean} True if the cards match, otherwise false if no match 44 | * @memberof Deck 45 | */ 46 | isSymbolMatch(cardDeck, firstCardIndex, secondCardIndex) { 47 | if (cardDeck[firstCardIndex].symbol === cardDeck[secondCardIndex].symbol) { 48 | return true; 49 | } 50 | return false; 51 | } 52 | 53 | /** 54 | * @description Shuffle a deck of game cards. This function is based on 55 | * http://stackoverflow.com/a/2450976 56 | * @returns {Object[]} Shuffled card deck 57 | * @memberof Deck 58 | */ 59 | shuffle() { 60 | let cardDeck = this.templateCardDeck; 61 | let currentIndex = cardDeck.length; 62 | let temporaryValue; 63 | let randomIndex; 64 | 65 | while (currentIndex !== 0) { 66 | randomIndex = Math.floor(Math.random() * currentIndex); 67 | currentIndex -= 1; 68 | temporaryValue = cardDeck[currentIndex]; 69 | cardDeck[currentIndex] = cardDeck[randomIndex]; 70 | cardDeck[randomIndex] = temporaryValue; 71 | } 72 | 73 | return cardDeck; 74 | } 75 | } 76 | 77 | export default Deck; 78 | -------------------------------------------------------------------------------- /COLLABORATOR_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Collaborator Guide 2 | 3 | As a collaborator you will be involved with axios with some administrative 4 | responsibilities. This guide will help you understand your role and the 5 | responsibilities that come with being a collaborator. 6 | 7 | 1. __Adhere to and help enforce the Code of Conduct.__ It is expected that you 8 | have read the [code of conduct](https://github.com/jdmedlock/memorygame/blob/development/CODE_OF_CONDUCT.md) 9 | and that you agree to live by it. This community should be friendly and 10 | welcoming. 11 | 12 | 1. __Triage issues.__ As a collaborator you may help sort through the issues 13 | that are reported. Issues vary from bugs, regressions, feature requests, 14 | questions, etc. Apply the appropriate label(s) and respond as needed. If it is 15 | a legitimate request please address it, otherwise feel free to close the issue 16 | and include a comment with a suggestion on where to find support. If an issue 17 | has been inactive for more than a week (i.e, the owner of the issue hasn’t 18 | responded to you), close the issue with a note indicating stales issues are 19 | closed; it can always be reopened if needed. In the case of issues that require 20 | a code change encourage the owner to submit a PR. For less complex code changes, 21 | add a very simple and detailed checklist, apply the “first-timers-only” label, 22 | and encourage a newcomer to open source to get involved. 23 | 24 | 1. __Answer questions.__ It is not expected that you provide answers to 25 | questions that aren’t relevant, nor do you need to mentor people on how to use 26 | JavaScript, etc. If the question is not directly about the module, please close 27 | the issue. If the question stems from poor documentation, please update the 28 | docs and consider adding a code example. In any event try to be helpful and 29 | remember that there’s no such thing as a stupid question. 30 | 31 | 1. __Assist with PRs.__ By encouraging contributors to supply a PR for their 32 | own issue this is ideally where most of your attention will be focused. Keep a 33 | few things in mind as you review PRs. 34 | - When fixing a bug: does the PR adequately solve the problem without 35 | introducing any regressions? 36 | - When implementing a feature: does the feature fit within the scope of axios? 37 | - When removing functionality: is it properly deprecated with a warning? 38 | - When introducing functionality: is the API predictable? 39 | - Does the new code work for all supported platforms/browsers? 40 | - Do the tests and linting pass CI? 41 | - Are there tests to validate the changes that have been made? 42 | 43 | 1. __Fix bugs and implement features.__ When things need to be fixed or 44 | implemented and a PR can’t wait, you may do things yourself. You should still 45 | submit a PR yourself and get it checked off by at least one other contributor. 46 | Keep the points from number 4 in consideration as you push your code. 47 | 48 | Thank you again for your help as a collaborator and in making axios community 49 | great! If you have any questions, or need any assistance please feel free to 50 | contact another collaborator or the owner. 51 | 52 | ## Attribution 53 | 54 | This guide is adapted from the [Axios](https://github.com/axios/axios) project. -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, 6 | *::before, 7 | *::after {box-sizing: inherit; 8 | } 9 | 10 | html, 11 | body { 12 | width: 100%;height: 100%; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | body { 18 | background: #ffffff url('../img/geometry2.png'); 19 | /* Background pattern from Subtle Patterns */ 20 | font-family: 'Coda', cursive; 21 | } 22 | 23 | .container { 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | flex-direction: column; 28 | } 29 | 30 | h1 { 31 | font-family: 'Open Sans', sans-serif; 32 | font-weight: 300; 33 | } 34 | 35 | /* 36 | * Styles for the deck of cards 37 | */ 38 | 39 | .deck { 40 | width: 660px; 41 | min-height: 680px; 42 | background: linear-gradient(160deg, #02ccba 0%, #aa7ecd 100%); 43 | padding: 32px; 44 | border-radius: 10px; 45 | box-shadow: 12px 15px 20px 0 rgba(46, 61, 73, 0.5); 46 | display: flex; 47 | flex-wrap: wrap; 48 | justify-content: space-between; 49 | align-items: center; 50 | margin: 0 0 3em; 51 | } 52 | 53 | .deck .card { 54 | height: 125px; 55 | width: 125px; 56 | background: #2e3d49; 57 | font-size: 0; 58 | color: #ffffff; 59 | border-radius: 8px; 60 | cursor: pointer; 61 | display: flex; 62 | justify-content: center; 63 | align-items: center; 64 | box-shadow: 5px 2px 20px 0 rgba(46, 61, 73, 0.5); 65 | } 66 | 67 | .deck .card.open { 68 | transform: rotateY(0); 69 | background: #02b3e4; 70 | cursor: default; 71 | } 72 | 73 | .deck .card.faceup { 74 | font-size: 33px; 75 | } 76 | 77 | .deck .card.match { 78 | cursor: default; 79 | background: #02ccba; 80 | font-size: 33px; 81 | } 82 | 83 | @keyframes card-match { 84 | from { 85 | height: 125px; 86 | width: 125px; 87 | background: #02b3e4; 88 | } 89 | 50% { 90 | height: 150px; 91 | width: 150px; 92 | } 93 | to { 94 | height: 125px; 95 | width: 125px; 96 | background: #02ccba; 97 | } 98 | } 99 | /* 100 | * Styles for the Score Panel 101 | */ 102 | 103 | .score-panel { 104 | text-align: left; 105 | width: 345px; 106 | margin-bottom: 10px; 107 | display: flex; 108 | justify-content: space-between; 109 | } 110 | 111 | .score-panel .stars { 112 | margin: 0; 113 | padding: 0; 114 | display: inline-block; 115 | margin: 0 5px 0 0; 116 | } 117 | 118 | .score-panel .stars li { 119 | list-style: none; 120 | display: inline-block; 121 | } 122 | 123 | .score-panel .restart { 124 | float: right; 125 | cursor: pointer; 126 | } 127 | 128 | /* 129 | * Styles for the Win Dialog 130 | */ 131 | 132 | .win-dialog { 133 | display: none; 134 | text-align: center; 135 | width: 100%; 136 | margin-bottom: 10px; 137 | justify-content: center; 138 | } 139 | 140 | .win-dialog .win-icon { 141 | font-size: 144px; 142 | color: #02ccba;; 143 | } 144 | 145 | .win-dialog .win-banner { 146 | font-size: 48px; 147 | } 148 | 149 | .win-dialog .win-summary { 150 | font-size: 32px; 151 | } 152 | 153 | .win-dialog .win-button { 154 | background-color: #02ccba; 155 | border: 1px solid #02b3e4; 156 | border-radius: 10px; 157 | font-size: 32px; 158 | padding: 10px; 159 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are currently in the early release stages and are not open to outside 4 | contributors. However, we plan on opening it up to additional contributors 5 | when we emerge from the early release stage. 6 | 7 | ### Code Style 8 | 9 | Please follow the 10 | [AirBnB Javascript Style Guide](https://github.com/airbnb/javascript). 11 | 12 | ### Commit Messages 13 | 14 | Commit messages should be formatted using the following pattern: 15 | ``` 16 | : 17 | 18 | 19 | 20 | Resolves: 21 | See also: 22 | ``` 23 | 24 | _Type_ describes the nature of the change and should be one of the following: 25 | 26 | - `feature`: a new feature 27 | - `fix`: a bug fix 28 | - `docs`: changes to documentation 29 | - `style`: formatting, missing semi colons, etc; no code change 30 | - `refactor`: refactoring production code 31 | - `test`: adding tests, refactoring test; no production code change 32 | - `other`: updating build tasks, package manager configs, etc; no production 33 | code change 34 | 35 | _Subject_ is a short imperative statement of no more than 50 characters that 36 | describes the intent of the commit. 37 | 38 | _Body_ provides a more detailed explanation of the context, why, and what of 39 | the changes included in the commit. Remember that the body shouldn't describe 40 | how the code operates. Comments within the code should describe how it 41 | functions when and where necessary. Be sure to separate the body from other 42 | parts of the commit message using blank lines. 43 | 44 | _Resolves_ documents one or more issues the commit closes. These should be 45 | specified as URL's to those issues. Specify this as 'N/a' if the commit isn't 46 | associated with an issue. 47 | 48 | _See also_ may be used to reference any other supporting documentation. For 49 | example, URL's to Gist's. 50 | 51 | ### Testing 52 | 53 | Please update the tests to reflect your code changes. Pull requests will not 54 | be accepted if they are failing 55 | on [Travis CI](https://travis-ci.org/jdmedlock/memorygame). 56 | 57 | ### Documentation 58 | 59 | Please update the docs accordingly so that there are no discrepencies between 60 | the API and the documentation. 61 | 62 | ### Developing 63 | 64 | *_TBD_* 65 | 66 | #### Git Branches 67 | 68 | ![MemoryGame Git Workflow](https://github.com/jdmedlock/memorygame/blob/master/docs/Git%20-%20Team%20Workflow.png) 69 | 70 | - `master`: Only updated from PR's from the `development` branch for release. 71 | This branch always reflects the current production release. 72 | - `development`: Reflects the candidate code for the next release. Developers 73 | work in working branches, which are then pulled into this branch. All code 74 | pulled into this branch must be tested and undergo peer review as part of the 75 | PR process. 76 | - `working branches`: Are individual branches created by each developer when 77 | they are working on changes and bug fixes. There are 4 basic types of branches: 78 | bug, feature, refactor and style, after the type comes the name, it should 79 | specify on top of the branch type. For example feature/course-review. 80 | 81 | 82 | Please don't include changes to `dist/` in your pull request. This should only 83 | be updated when releasing a new version. 84 | 85 | ### Releasing 86 | 87 | *_TBD_* 88 | 89 | ### Running Examples 90 | 91 | *_TBD_* -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jdmedlock@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from 71 | 72 | - [Axios](https://github.com/axios/axios) 73 | - [Contributor Covenant][homepage], version 1.4, available at 74 | [http://contributor-covenant.org/version/1/4][version] 75 | 76 | [homepage]: http://contributor-covenant.org 77 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memory Game Project 2 | 3 | [![MemoryGame last commit](https://img.shields.io/github/last-commit/google/skia.svg)](https://github.com/jdmedlock/memorygame) 4 |
5 | [![Packagist](https://img.shields.io/packagist/l/doctrine/orm.svg)](https://github.com/jdmedlock/memorygame/) 6 | 7 | ## Table of Contents 8 | 9 | * [Overview](#overview) 10 | * [How to Play](#how-to-play) 11 | * [Player UI Feature](#player-ui-features) 12 | * [Dependencies](#dependencies) 13 | * [Change Log](#change-log) 14 | * [Contributing](#contributing) 15 | * [Authors](#authors) 16 | * [License](#license) 17 | 18 | ## Overview 19 | 20 | The Memory Game project was created as part of the Web Programming with 21 | Javascript section of the [Udacity Front-End Web Developer Nanodegree Program](https://www.udacity.com/course/front-end-web-developer-nanodegree--nd001). The 22 | purpose of this assignment is to demonstrate mastery of the core web 23 | development skills - HTML, CSS, and JavaScript. 24 | 25 | You can play the game here --> [Memory Game](https://jdmedlock.github.io/memorygame/) 26 | 27 | ## How to Play 28 | 29 | This game is a browser-based card matching game that presents the player with 30 | cards arranged in a 4x4 grid. On one side of each card is a common design 31 | shared by all cards. On the other is a distinctive symbol shared by one pair 32 | of cards in the deck, thus there are 8 unique symbols shared by 8 pairs of cards 33 | in the deck. 34 | 35 | The objective of the Memory Game is for the play to turn over pairs of matching 36 | cards across eight successive turns. In a turn if the player selects two cards 37 | whose symbols match those cards, along with those successfully matched in 38 | previous turns, will remain up. However, if the player chooses two cards with 39 | different symbols they will both be flipped back over. 40 | 41 | The game ends when all eight pairs of matching cards have been revealed. 42 | 43 | ## Player UI Features 44 | 45 | In addition to the basic game play several UI components have been implemented 46 | to provide the player with features to improve the overall experience. 47 | 48 | * Restart Button - This button give the player the means to reset the game 49 | board, timer, and star rating. 50 | 51 | * Star Rating - From 1 to 3 stars are displayed to provide the player with 52 | a visual indication of his or her performance. Three stars are displayed at the 53 | start of the first turn and will be decremented by one star when the player 54 | fails to match cards in a turn. A star will be added when a turn is "won", 55 | but at any point in time a minimum of 0 stars and a maximum of 3 stars will 56 | be displayed. 57 | 58 | * Timer - A timer displaying the number of minutes and seconds that have 59 | elapsed. The timer is stopped when the player wins the game. 60 | 61 | * Move Counter - Displays the number of turns the player has taken, starting 62 | with one at the first turn. 63 | 64 | ## Dependencies 65 | 66 | This app has the following dependencies 67 | 68 | | Module/Library | Environment | Description | Related Files | 69 | |:---------------|:------------|:------------|:--------------| 70 | | NPM | Development | Package manager | package.json | 71 | | WebPack | Development | Bundler | webpack.config.js | 72 | 73 | To build the production application bundle, `/dist/bundle-app.js` issue the 74 | command `npm run build` from the command line. This bundle must be referenced 75 | in the file `index.html` using the `` 76 | tag at the bottom of the `` section of the source page. 77 | 78 | ## Change Log 79 | 80 | For more information see [Change Log](https://github.com/jdmedlock/memorygame/blob/development/CHANGELOG.md) 81 | 82 | ## Contributing 83 | 84 | See [Contributing](https://github.com/jdmedlock/memorygame/blob/development/CONTRIBUTING.md) 85 | and our [Collaborator Guide](https://github.com/jdmedlock/memorygame/blob/development/COLLABORATOR_GUIDE.md). 86 | 87 | ## Authors 88 | 89 | Developers on this project can be found on the [Contributors](https://github.com/jdmedlock/memorygame/graphs/contributors) page of this repo. 90 | 91 | ## License 92 | 93 | [MIT](https://tldrlegal.com/license/mit-license) 94 | 95 | -------------------------------------------------------------------------------- /js/GamePlay.js: -------------------------------------------------------------------------------- 1 | 2 | const MIN_PLAYER_RATING = 0; 3 | const MAX_PLAYER_RATING = 3; 4 | const TWO_SECONDS = 1000; 5 | const MATCH_LIMIT = 8; 6 | 7 | class GamePlay { 8 | /** 9 | * @description Creates an instance of the Game class. 10 | * 11 | * Note that the wait function used within this class was taken from 12 | * https://hackernoon.com/lets-make-a-javascript-wait-function-fa3a2eb88f11 13 | * @memberof GamePlay 14 | */ 15 | constructor() { 16 | this.deck = null; 17 | this.gameDeck = []; 18 | this.gameUI = null; 19 | this.playerRating = MAX_PLAYER_RATING; 20 | this.moveCount = 0; 21 | this.flipCount = 0; 22 | this.matchCount = 0; 23 | this.firstCard = undefined; 24 | this.deckFragment = null; 25 | this.wait = ms => new Promise((r, j) => setTimeout(r, ms)); 26 | this.isTurnInprogress = false; 27 | } 28 | 29 | /** 30 | * @description Set the reference to the Deck object 31 | * @param {Object} deck Reference to an instance of the Deck class 32 | * @memberof GamePlay 33 | */ 34 | setDeck(deck) { 35 | this.deck = deck; 36 | } 37 | 38 | /** 39 | * @description Set the reference to the GameUI object 40 | * @param {Object} gameUI Reference to an instance of the GameUI class 41 | * @memberof GamePlay 42 | */ 43 | setGameUI(gameUI) { 44 | this.gameUI = gameUI; 45 | } 46 | 47 | /** 48 | * @description Retrieve the game deck 49 | * @returns {Object[]} Game deck 50 | * @memberof GamePlay 51 | */ 52 | getGameDeck() { 53 | return this.gameDeck; 54 | } 55 | 56 | /** 57 | * @description Start a new game by shuffling the template card deck 58 | * to create a new game deck 59 | * @memberof GamePlay 60 | */ 61 | startNewGame() { 62 | this.playerRating = MAX_PLAYER_RATING; 63 | this.gameUI.updatePlayerRating(this.playerRating, MAX_PLAYER_RATING); 64 | this.moveCount = 0; 65 | this.gameUI.updateMoveCount(this.moveCount); 66 | this.flipCount = 0; 67 | this.matchCount = 0; 68 | this.firstCard = undefined; 69 | this.gameDeck = this.deck.shuffle(); 70 | this.gameUI.buildDeck(this.gameDeck); 71 | this.gameDeck.forEach((cardElement, cardIndex) => { 72 | this.gameUI.turnCardFaceDown(cardIndex); 73 | }); 74 | this.gameUI.startTimer(); 75 | } 76 | 77 | /** 78 | * @description Control a turn within the game. Within each turn the player 79 | * flips over a pair of cards If both cards have matching symbols they will 80 | * remain up. However, if the player chooses two cards with different symbols 81 | * they will both be flipped back over. 82 | * @param {Number} cardIndex Index of the selected card in the deck. 83 | * @returns {Boolean} True if last turn, otherwise false is returned 84 | * @memberof GamePlay 85 | */ 86 | turn(selectedCardIndex) { 87 | // Ensure we have a valid card index and the selected card wasn't matched 88 | // in a previous turn 89 | if (selectedCardIndex === null) { 90 | return false; 91 | } 92 | if (this.firstCard === selectedCardIndex) { 93 | return false; 94 | } 95 | if (this.gameUI.isCardMatched(selectedCardIndex)) { 96 | return false; 97 | } 98 | // Ignore clicks until the preceeding pair of cards have been evaluated 99 | if (this.flipCount > 1) { 100 | return false; 101 | } 102 | 103 | this.gameUI.turnCardFaceUp(selectedCardIndex); 104 | this.flipCount += 1; 105 | if (this.flipCount === 1) { 106 | this.firstCard = selectedCardIndex; 107 | } else { 108 | this.moveCount += 1; 109 | this.gameUI.updateMoveCount(this.moveCount); 110 | if (!this.deck.isSymbolMatch(this.gameDeck, this.firstCard, selectedCardIndex)) { 111 | this.pairNotMatched(this.firstCard, selectedCardIndex); 112 | } else { 113 | this.pairMatched(this.firstCard, selectedCardIndex); 114 | } 115 | } 116 | 117 | // Check for the end of the current game 118 | if (this.matchCount >= MATCH_LIMIT) { 119 | this.gameUI.stopTimer(); 120 | this.gameUI.showWinDialog(this, this.playerRating, this.moveCount); 121 | return true; 122 | } 123 | return false; 124 | } 125 | 126 | /** 127 | * @description Process a pair of cards matched by the user 128 | * @param {Number} firstCardCard Index of the first card of the pair in the deck 129 | * @param {Number} secondCardCard Index of the second card of the pair in the deck 130 | * @memberof GamePlay 131 | */ 132 | pairMatched(firstCardIndex, secondCardIndex) { 133 | this.matchCount += 1; 134 | this.gameUI.markMatchedPair(firstCardIndex, secondCardIndex); 135 | this.firstCard = undefined; 136 | this.flipCount = 0; 137 | this.playerRating = this.playerRating < MAX_PLAYER_RATING 138 | ? this.playerRating += 1 139 | : this.playerRating; 140 | this.gameUI.updatePlayerRating(this.playerRating, MAX_PLAYER_RATING); 141 | } 142 | 143 | /** 144 | * @description Process a pair of selected cards whose symbols don't match 145 | * @param {Number} firstCardCard Index of the first card of the pair in the deck 146 | * @param {Number} secondCardCard Index of the second card of the pair in the deck 147 | * @memberof GamePlay 148 | */ 149 | async pairNotMatched(firstCardIndex, secondCardIndex) { 150 | await this.wait(TWO_SECONDS); 151 | this.gameUI.turnCardFaceDown(firstCardIndex); 152 | this.gameUI.turnCardFaceDown(secondCardIndex); 153 | this.firstCard = undefined; 154 | this.flipCount = 0; 155 | this.playerRating = this.playerRating > MIN_PLAYER_RATING 156 | ? this.playerRating -= 1 157 | : this.playerRating; 158 | this.gameUI.updatePlayerRating(this.playerRating, MAX_PLAYER_RATING); 159 | } 160 | 161 | } 162 | 163 | export default GamePlay; 164 | -------------------------------------------------------------------------------- /dist/bundle-app.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function a(i){if(t[i])return t[i].exports;var s=t[i]={i:i,l:!1,exports:{}};return e[i].call(s.exports,s,s.exports,a),s.l=!0,s.exports}a.m=e,a.c=t,a.d=function(e,t,i){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(a.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)a.d(i,s,function(t){return e[t]}.bind(null,s));return i},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="",a(a.s=1)}([function(e,t,a){"use strict";a.r(t);const i=0,s=3,r=1e3,n=8;var o=class{constructor(){this.deck=null,this.gameDeck=[],this.gameUI=null,this.playerRating=s,this.moveCount=0,this.flipCount=0,this.matchCount=0,this.firstCard=void 0,this.deckFragment=null,this.wait=(e=>new Promise((t,a)=>setTimeout(t,e))),this.isTurnInprogress=!1}setDeck(e){this.deck=e}setGameUI(e){this.gameUI=e}getGameDeck(){return this.gameDeck}startNewGame(){this.playerRating=s,this.gameUI.updatePlayerRating(this.playerRating,s),this.moveCount=0,this.gameUI.updateMoveCount(this.moveCount),this.flipCount=0,this.matchCount=0,this.firstCard=void 0,this.gameDeck=this.deck.shuffle(),this.gameUI.buildDeck(this.gameDeck),this.gameDeck.forEach((e,t)=>{this.gameUI.turnCardFaceDown(t)}),this.gameUI.startTimer()}turn(e){return null!==e&&this.firstCard!==e&&!this.gameUI.isCardMatched(e)&&!(this.flipCount>1)&&(this.gameUI.turnCardFaceUp(e),this.flipCount+=1,1===this.flipCount?this.firstCard=e:(this.moveCount+=1,this.gameUI.updateMoveCount(this.moveCount),this.deck.isSymbolMatch(this.gameDeck,this.firstCard,e)?this.pairMatched(this.firstCard,e):this.pairNotMatched(this.firstCard,e)),this.matchCount>=n&&(this.gameUI.stopTimer(),this.gameUI.showWinDialog(this,this.playerRating,this.moveCount),!0))}pairMatched(e,t){this.matchCount+=1,this.gameUI.markMatchedPair(e,t),this.firstCard=void 0,this.flipCount=0,this.playerRating=this.playerRatingi?this.playerRating-=1:this.playerRating,this.gameUI.updatePlayerRating(this.playerRating,s)}};var c=class{constructor(){this.gameTimer=null,this.gameTimerMinutes=0,this.gameTimerSeconds=0,this.secondsDOMElement=document.querySelector(".timer-seconds"),this.minutesDOMElement=document.querySelector(".timer-minutes")}buildDeck(e){const t=document.querySelector(".deck");if(t.childElementCount>0)for(;t.firstChild;)t.removeChild(t.firstChild);const a=document.createDocumentFragment();e.forEach((e,t)=>{const i=document.createElement("li");i.setAttribute("id",`${t}`),i.setAttribute("class","card");const s=document.createElement("i");s.setAttribute("class",`fa ${e.symbol}`),i.appendChild(s),a.appendChild(i)}),t.appendChild(a)}turnCardFaceDown(e){document.getElementById(`${e}`).setAttribute("class","card")}turnCardFaceUp(e){const t=document.getElementById(`${e}`),a=t.getAttribute("class")+" open faceup ";t.setAttribute("class",a)}markMatchedPair(e,t){const a=document.getElementById(`${e}`);let i=a.getAttribute("class")+" match ";a.setAttribute("class",i);const s=document.getElementById(`${t}`);i=s.getAttribute("class")+" match",s.setAttribute("class",i),this.animateMatchedPair(a,s)}isCardMatched(e){return document.getElementById(`${e}`).getAttribute("class").includes("match")}animateMatchedPair(e,t){const a="animation-duration: 1s; animation-name: card-match;";e.setAttribute("style",a),t.setAttribute("style",a)}updateMoveCount(e){document.querySelector(".moves").innerText=e}updatePlayerRating(e,t){const a=document.querySelectorAll(".rating");for(let i=0;i=60&&(e.gameTimerSeconds=0,e.gameTimerMinutes+=1,e.minutesDOMElement.innerText=("0"+e.gameTimerMinutes).slice(-2)),e.secondsDOMElement.innerText=("0"+e.gameTimerSeconds).slice(-2)}stopTimer(){null!==this.gameTimer&&(clearInterval(this.gameTimer),this.gameTimer=null)}showWinDialog(e,t,a){document.querySelector(".game-board").setAttribute("style","display: none"),document.querySelector(".win-minutes").innerText=this.gameTimerMinutes,document.querySelector(".win-seconds").innerText=this.gameTimerSeconds,document.querySelector(".win-moves").innerText=a,document.querySelector(".win-stars").innerText=t;const i=document.querySelector(".win-button");i.gamePlayRef=e,i.addEventListener("click",this.setupForNewGame),document.querySelector(".win-dialog").setAttribute("style","display: flex")}setupForNewGame(e){document.querySelector(".win-dialog").setAttribute("style","display: none"),document.querySelector(".game-board").setAttribute("style","display: flex"),e.target.gamePlayRef.startNewGame()}};const m=new class{constructor(){this.templateCardDeck=[{symbol:"fa-diamond",faceup:!1,matched:!1},{symbol:"fa-diamond",faceup:!1,matched:!1},{symbol:"fa-paper-plane-o",faceup:!1,matched:!1},{symbol:"fa-paper-plane-o",faceup:!1,matched:!1},{symbol:"fa-anchor",faceup:!1,matched:!1},{symbol:"fa-anchor",faceup:!1,matched:!1},{symbol:"fa-bolt",faceup:!1,matched:!1},{symbol:"fa-bolt",faceup:!1,matched:!1},{symbol:"fa-cube",faceup:!1,matched:!1},{symbol:"fa-cube",faceup:!1,matched:!1},{symbol:"fa-leaf",faceup:!1,matched:!1},{symbol:"fa-leaf",faceup:!1,matched:!1},{symbol:"fa-bicycle",faceup:!1,matched:!1},{symbol:"fa-bicycle",faceup:!1,matched:!1},{symbol:"fa-bomb",faceup:!1,matched:!1},{symbol:"fa-bomb",faceup:!1,matched:!1}]}isSymbolMatch(e,t,a){return e[t].symbol===e[a].symbol}shuffle(){let e,t,a=this.templateCardDeck,i=a.length;for(;0!==i;)t=Math.floor(Math.random()*i),e=a[i-=1],a[i]=a[t],a[t]=e;return a}},u=new o,l=new c;u.setDeck(m),u.setGameUI(l),u.startNewGame();document.querySelector(".deck");document.querySelector(".deck").addEventListener("click",e=>{u.turn(e.target.getAttribute("id"))}),document.querySelector(".restart").addEventListener("click",e=>{u.startNewGame()})},function(e,t,a){e.exports=a(0)}]); -------------------------------------------------------------------------------- /js/GameUI.js: -------------------------------------------------------------------------------- 1 | 2 | class GameUI { 3 | 4 | /** 5 | * @description Create and instance of GameUI 6 | * @memberof GameUI 7 | */ 8 | constructor() { 9 | this.gameTimer = null; 10 | this.gameTimerMinutes = 0; 11 | this.gameTimerSeconds = 0; 12 | this.secondsDOMElement = document.querySelector('.timer-seconds'); 13 | this.minutesDOMElement = document.querySelector('.timer-minutes'); 14 | } 15 | 16 | /** 17 | * @description Build a DOM document fragment containing the cards the 18 | * user will interact with in a game 19 | * @param {Object[]} gameDeck Cards in the current game deck 20 | * @memberof GameUI 21 | */ 22 | buildDeck(gameDeck) { 23 | const deckElement = document.querySelector('.deck'); 24 | if (deckElement.childElementCount > 0) { 25 | while (deckElement.firstChild) { 26 | deckElement.removeChild(deckElement.firstChild); 27 | } 28 | } 29 | const deckFragment = document.createDocumentFragment(); 30 | gameDeck.forEach((card, cardIndex) => { 31 | const liElement = document.createElement('li'); 32 | liElement.setAttribute('id', `${cardIndex}`); 33 | liElement.setAttribute('class', 'card'); 34 | const iElement = document.createElement('i'); 35 | iElement.setAttribute('class', `fa ${card.symbol}`); 36 | liElement.appendChild(iElement); 37 | deckFragment.appendChild(liElement); 38 | }); 39 | deckElement.appendChild(deckFragment); 40 | } 41 | 42 | /** 43 | * @description Turn a card facedown on the game board 44 | * @param {Number} selectedCard Index of the selected card in the deck 45 | * @memberof GameUI 46 | */ 47 | turnCardFaceDown(selectedCardIndex) { 48 | const selectedCard = document.getElementById(`${selectedCardIndex}`); 49 | selectedCard.setAttribute('class', 'card'); 50 | } 51 | 52 | /** 53 | * @description Turn a card faceup on the game board 54 | * @param {Number} selectedCard Index of the selected card in the deck 55 | * @memberof GameUI 56 | */ 57 | turnCardFaceUp(selectedCardIndex) { 58 | const selectedCard = document.getElementById(`${selectedCardIndex}`); 59 | const cardAttributes = selectedCard.getAttribute('class') + ' open faceup '; 60 | selectedCard.setAttribute('class', cardAttributes); 61 | } 62 | 63 | /** 64 | * @description Mark the selected card as being matched 65 | * @param {Number} firstCardCard Index of the first card of the pair in the deck 66 | * @param {Number} secondCardCard Index of the second card of the pair in the deck 67 | * @memberof GameUI 68 | */ 69 | markMatchedPair(firstCardIndex, secondCardIndex) { 70 | const firstSelectedCard = document.getElementById(`${firstCardIndex}`); 71 | let cardAttributes = firstSelectedCard.getAttribute('class') + ' match '; 72 | firstSelectedCard.setAttribute('class', cardAttributes); 73 | const secondSelectedCard = document.getElementById(`${secondCardIndex}`); 74 | cardAttributes = secondSelectedCard.getAttribute('class') + ' match'; 75 | secondSelectedCard.setAttribute('class', cardAttributes); 76 | this.animateMatchedPair(firstSelectedCard, secondSelectedCard); 77 | } 78 | 79 | /** 80 | * @description Check if a card has been previously matched 81 | * @param {Number} cardIndex Index of the card to check 82 | * @returns {Boolean} true if the card was previously matched, otherwise false 83 | * @memberof GameUI 84 | */ 85 | isCardMatched(cardIndex) { 86 | return document.getElementById(`${cardIndex}`) 87 | .getAttribute('class') 88 | .includes('match'); 89 | } 90 | 91 | /** 92 | * @description Animate a pair of cards successfully matched by the player 93 | * @param {*} firstSelectedCard DOM element of the first matched card 94 | * @param {*} secondSelectedCard DOM element of the second matched card 95 | * @memberof GameUI 96 | */ 97 | animateMatchedPair(firstSelectedCard, secondSelectedCard) { 98 | const matchedPairStyle = 'animation-duration: 1s; animation-name: card-match;'; 99 | firstSelectedCard.setAttribute("style", matchedPairStyle); 100 | secondSelectedCard.setAttribute("style", matchedPairStyle); 101 | } 102 | 103 | /** 104 | * @description Display the current turn count (i.e. moves) 105 | * @param {Number} moveCount Number of turns the player has made in the 106 | * current game 107 | * @memberof GameUI 108 | */ 109 | updateMoveCount(moveCount) { 110 | const countElement = document.querySelector('.moves'); 111 | countElement.innerText = moveCount; 112 | } 113 | 114 | /** 115 | * @description Display the current player star rating 116 | * @param {Number} starCount Players current star rating 117 | * @param {Number} starLimit Maximum possible number of stars 118 | * @memberof GameUI 119 | */ 120 | updatePlayerRating(starCount, starLimit) { 121 | const closedStarClasses = 'rating fa fa-star'; 122 | const openStarClasses = 'rating fa fa-star-o'; 123 | const ratingNodeList = document.querySelectorAll('.rating'); 124 | for (let i = 0; i < starLimit; i += 1) { 125 | if ((starCount - i) <= 0) { 126 | ratingNodeList[i].setAttribute('class', openStarClasses); 127 | } else { 128 | ratingNodeList[i].setAttribute('class', closedStarClasses); 129 | } 130 | } 131 | } 132 | 133 | /** 134 | * @description Start a new game timer 135 | * @memberof GameUI 136 | */ 137 | startTimer() { 138 | this.stopTimer(); 139 | this.gameTimerMinutes = 0; 140 | this.minutesDOMElement.innerText = '00'; 141 | this.gameTimerSeconds = 0; 142 | this.secondsDOMElement.innerText = '00'; 143 | this.gameTimer = setInterval(this.showNewTime, 1000, this); 144 | } 145 | 146 | /** 147 | * @description Update the game timer and add the results to the DOM 148 | * @memberof GameUI 149 | */ 150 | showNewTime(gameui) { 151 | gameui.gameTimerSeconds += 1; 152 | if (gameui.gameTimerSeconds >= 60) { 153 | gameui.gameTimerSeconds = 0; 154 | gameui.gameTimerMinutes += 1; 155 | gameui.minutesDOMElement.innerText = ("0" + gameui.gameTimerMinutes).slice(-2); 156 | } 157 | gameui.secondsDOMElement.innerText = ("0" + gameui.gameTimerSeconds).slice(-2); 158 | } 159 | 160 | /** 161 | * @description Stop the game timer if one is currently active 162 | * @memberof GameUI 163 | */ 164 | stopTimer() { 165 | if (this.gameTimer !== null) { 166 | clearInterval(this.gameTimer); 167 | this.gameTimer = null; 168 | } 169 | } 170 | 171 | /** 172 | * @description Display the game win dialog with play metrics 173 | * @param {*} gamePlay 174 | * @param {*} playerRating 175 | * @param {*} moveCount 176 | * @memberof GameUI 177 | */ 178 | showWinDialog(gamePlay, playerRating, moveCount) { 179 | document.querySelector('.game-board').setAttribute('style', 'display: none'); 180 | 181 | document.querySelector('.win-minutes').innerText = this.gameTimerMinutes; 182 | document.querySelector('.win-seconds').innerText = this.gameTimerSeconds; 183 | document.querySelector('.win-moves').innerText = moveCount; 184 | document.querySelector('.win-stars').innerText = playerRating; 185 | 186 | const winButton = document.querySelector('.win-button'); 187 | winButton.gamePlayRef = gamePlay; // Make gamePlay available to event handler 188 | winButton.addEventListener('click', this.setupForNewGame); 189 | document.querySelector('.win-dialog').setAttribute('style', 'display: flex'); 190 | } 191 | 192 | /** 193 | * @description Win Button vent handler. Note that the 'win-button' element 194 | * is expected to contain a 'gamePlayRef' attribute containing the reference 195 | * to the GamePlay object instance. 196 | * @param {*} event The event that was triggered 197 | * @memberof GameUI 198 | */ 199 | setupForNewGame(event) { 200 | document.querySelector('.win-dialog').setAttribute('style', 'display: none'); 201 | document.querySelector('.game-board').setAttribute('style', 'display: flex'); 202 | event.target.gamePlayRef.startNewGame(); 203 | } 204 | 205 | } 206 | 207 | export default GameUI; 208 | --------------------------------------------------------------------------------