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 | 
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 | [](https://github.com/jdmedlock/memorygame)
4 |
5 | [](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 |
--------------------------------------------------------------------------------