props.onChangePeg(index)}
20 | style={{ color: rows[index] }} />
21 | );
22 | }
23 |
24 | return (
25 |
26 | {codePegs}
27 |
28 | );
29 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "master-mind",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "https://robinhuy.github.io/master-mind",
6 | "dependencies": {
7 | "@material-ui/core": "^4.1.1",
8 | "@material-ui/icons": "^4.2.0",
9 | "intro.js": "^2.9.3",
10 | "react": "^16.8.6",
11 | "react-dom": "^16.8.6",
12 | "react-scripts": "3.0.1"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/HiddenPegs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid } from '@material-ui/core';
3 | import { Lens, HelpOutline } from '@material-ui/icons';
4 |
5 | import './Main.css';
6 |
7 | export default function HiddenPegs(props) {
8 | const { codes, isWin } = props;
9 | let codePegs = [];
10 |
11 | for (let i = 0; i < codes.length; i++) {
12 | if (isWin === null) {
13 | codePegs.push(
14 |
18 | );
19 | } else {
20 | codePegs.push(
21 |
26 | );
27 | }
28 | }
29 |
30 | return (
31 |
32 | {codePegs}
33 |
34 | );
35 | }
--------------------------------------------------------------------------------
/src/components/dialogs/IntroDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core';
3 |
4 | export default function IntroDialog(props) {
5 | const { openDialog, onCloseDialog, showIntro } = props
6 | return (
7 |
29 | )
30 | }
--------------------------------------------------------------------------------
/src/components/ColorPegs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid, Button } from '@material-ui/core';
3 | import { Lens, Check } from '@material-ui/icons';
4 |
5 | import './Main.css';
6 |
7 | export default function ColorPegs(props) {
8 | const isMobile = props.isMobile;
9 |
10 | return (
11 |
17 |
18 | {props.colors.map(color =>
19 |
20 | props.onChooseColor(color)} />
25 |
26 | )}
27 |
28 |
29 |
32 |
33 | );
34 | }
--------------------------------------------------------------------------------
/src/components/KeyPegs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Grid } from '@material-ui/core';
3 | import { SwapHorizontalCircle, CheckCircle, PanoramaFishEye } from '@material-ui/icons';
4 |
5 | export default function KeyPegs(props) {
6 | const { isMobile, keys, numberOfPegsInRow, rowIndex } = props;
7 | const fontSize = isMobile ? 'small' : 'default';
8 | let gridItem = [];
9 |
10 | for (let i = 0; i < props.numberOfPegsInRow; i++) {
11 | const index = rowIndex * numberOfPegsInRow + i;
12 |
13 | gridItem.push(
14 |
15 |
16 | {keys[index] === 'black' ? (
17 |
18 | ) : keys[index] === 'white' ? (
19 |
20 | ) : (
21 |
22 | )}
23 |
24 |
25 | )
26 | }
27 |
28 | return (
29 |
30 | {gridItem}
31 |
32 | );
33 | }
--------------------------------------------------------------------------------
/src/components/Main.css:
--------------------------------------------------------------------------------
1 | .Board-Row {
2 | border-top: 1px solid #ddd;
3 | padding-left: 8px;
4 | padding-right: 8px;
5 | }
6 |
7 | .Board-Row--active {
8 | background-color: #f5f5f5;
9 | }
10 |
11 | .Code-Peg {
12 | cursor: pointer;
13 | border: 1px solid #ffffff;
14 | border-radius: 100%;
15 | }
16 |
17 | .Code-Peg--active, .Code-Peg--win, .Code-Peg--lose {
18 | border-radius: 100%;
19 | }
20 |
21 | .Code-Peg--active {
22 | border: 1px solid #aaaaaa;
23 | }
24 |
25 | .Code-Peg--win {
26 | border: 1px solid #85C920;
27 | }
28 |
29 | .Code-Peg--lose {
30 | border: 1px solid #FF4B38;
31 | }
32 |
33 | .Color-Pegs {
34 | position: relative;
35 | padding: 5px;
36 | text-align: center;
37 | background-color: #ffffff;
38 | border: 1px solid #ddd;
39 | }
40 |
41 | .Color-Pegs::before {
42 | content: "";
43 | position: absolute;
44 | width: 0;
45 | height: 0;
46 | top: 50%;
47 | left: -13px;
48 | box-sizing: border-box;
49 | border: 10px solid black;
50 | border-color: #ffffff transparent transparent #ffffff;
51 | transform-origin: 0 0;
52 | transform: rotate(-45deg);
53 | box-shadow: -1px -1px 0px -1px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), -1px -1px 0px 0px rgba(0, 0, 0, 0.12)
54 | }
55 |
56 | .Color-Peg {
57 | cursor: pointer;
58 | }
59 |
60 | .Color-Peg:hover {
61 | opacity: 0.9;
62 | }
63 |
64 | .Color-Peg:active {
65 | opacity: 0.6;
66 | }
67 |
68 | .Step-Number {
69 | text-align: center;
70 | font-size: 1.5rem;
71 | font-weight: bold;
72 | color: #6a6a6a;
73 | }
--------------------------------------------------------------------------------
/src/components/dialogs/ResultDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core';
3 | import { Refresh } from '@material-ui/icons';
4 |
5 | import clover from './clover.png';
6 | import cup from './cup.png';
7 | import blackRaven from './black-raven.png';
8 | import tinyBrain from './tiny-brain.jpg';
9 |
10 | export default function ResultDialog(props) {
11 | const { openDialog, isWin, currentRow, onCloseDialog, onRestartGame, hasRestartedGame } = props
12 |
13 | let textContent = '';
14 | let image = null;
15 |
16 | if (isWin) {
17 | if (currentRow < 2) {
18 | textContent = 'You are the luckies person in the world!';
19 | image = clover;
20 | } else {
21 | textContent = 'You are the champion!';
22 | image = cup;
23 | }
24 | } else {
25 | if (!hasRestartedGame) {
26 | textContent = 'Better luck next time!';
27 | image = blackRaven;
28 | } else {
29 | textContent = 'You must think harder!';
30 | image = tinyBrain;
31 | }
32 | }
33 |
34 | return (
35 |
62 | )
63 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
30 |
31 |
32 |
33 | Master Mind
34 |
35 |
43 |
44 |
45 |
46 |
47 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/components/Board.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paper, List, ListItem, Grid, Button } from '@material-ui/core';
3 | import { Refresh } from '@material-ui/icons';
4 |
5 | import KeyPegs from './KeyPegs';
6 | import CodePegs from './CodePegs';
7 | import ColorPegs from './ColorPegs';
8 | import HiddenPegs from './HiddenPegs';
9 |
10 | export default function Board(props) {
11 | const { isMobile, numberOfRows, numberOfPegsInRow, codes, rows, keys, currentRow, currentPeg, colors, isWin, onRestartGame } = props
12 | let listItems = [];
13 |
14 | for (let i = 0; i < numberOfRows; i++) {
15 | const isCurrentRow = (currentRow === i);
16 |
17 | listItems.push(
18 |
23 |
24 | {i === 0 ? (
25 |
31 |
32 |
33 | ) : (
34 |
35 |
36 |
37 | )}
38 |
39 | {i === 0 ? (
40 |
46 |
54 |
55 | ) : (
56 |
57 |
65 |
66 | )}
67 |
68 |
69 | {isCurrentRow && isWin === null &&
70 |
76 | }
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | const hiddenRowIndex = numberOfRows + 1
84 | listItems.push(
85 |
91 |
92 |
93 | {currentRow}
94 |
95 |
96 |
102 |
103 |
104 |
105 |
106 |
107 |
110 |
111 |
112 |
113 |
114 | )
115 |
116 | return (
117 |
122 | {listItems}
123 |
124 | );
125 | }
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container, AppBar, Toolbar, Typography } from '@material-ui/core';
3 | import introJs from 'intro.js';
4 | import 'intro.js/minified/introjs.min.css';
5 |
6 | import Board from './components/Board';
7 | import ResultDialog from './components/dialogs/ResultDialog';
8 | import IntroDialog from './components/dialogs/IntroDialog';
9 |
10 | class App extends React.Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | colors: ['#FFB400', '#FF5A5F', '#8CE071', '#00D1C1', '#007A87', '#7B0051'],
15 | numberOfRows: 8,
16 | numberOfPegsInRow: 4,
17 | codes: [],
18 | rows: [],
19 | keys: [],
20 | currentRow: 0,
21 | currentPeg: 0,
22 | openIntroDialog: false,
23 | openResultDialog: false,
24 | isWin: null,
25 | hasRestartedGame: false,
26 | isMobile: false
27 | };
28 | }
29 |
30 | _generateRandomCodes = () => {
31 | const { colors, numberOfPegsInRow } = this.state;
32 | let codes = [];
33 |
34 | for (let i = 0; i < numberOfPegsInRow; i++) {
35 | codes.push(colors[Math.floor(Math.random() * colors.length)]);
36 | }
37 |
38 | return codes;
39 | }
40 |
41 | _initGame = () => {
42 | const { numberOfPegsInRow, numberOfRows } = this.state;
43 | const length = numberOfRows * numberOfPegsInRow;
44 |
45 | this.setState({
46 | codes: this._generateRandomCodes(),
47 | rows: Array(length).fill('gray'),
48 | keys: Array(length).fill('gray'),
49 | currentRow: 0,
50 | currentPeg: 0,
51 | openResultDialog: false,
52 | isWin: null
53 | });
54 | }
55 |
56 | componentDidMount() {
57 | this._initGame();
58 |
59 | if (window.matchMedia && window.matchMedia('(max-width: 500px)').matches) {
60 | this.setState({ isMobile: true })
61 | }
62 |
63 | // Only show intro dialog if it's the first time visiting page
64 | const isVisited = localStorage.getItem('isVisited');
65 | if (!isVisited) {
66 | this.setState({ openIntroDialog: true })
67 | }
68 | }
69 |
70 | _showIntro = () => {
71 | this.setState({ openIntroDialog: false })
72 | introJs().setOption('showStepNumbers', false).start();
73 | localStorage.setItem('isVisited', '1');
74 | }
75 |
76 | _onChooseColor = (color) => {
77 | const { rows, currentPeg, numberOfPegsInRow } = this.state;
78 | let newRows = Array.from(rows);
79 | newRows[currentPeg] = color;
80 |
81 | this.setState({ rows: newRows });
82 |
83 | // Only change the current peg when it is not the final peg in row
84 | if ((currentPeg + 1) % numberOfPegsInRow !== 0) {
85 | this.setState({ currentPeg: currentPeg + 1 });
86 | }
87 | }
88 |
89 | _onChangePeg = (index) => {
90 | const { currentRow, numberOfPegsInRow } = this.state;
91 | const startIndex = currentRow * numberOfPegsInRow;
92 | const endIndex = startIndex + numberOfPegsInRow;
93 |
94 | if (index >= startIndex && index < endIndex) {
95 | this.setState({ currentPeg: index });
96 | }
97 | }
98 |
99 | _onSubmit = () => {
100 | const { codes, rows, keys, currentRow, numberOfRows, numberOfPegsInRow } = this.state;
101 | const startIndex = currentRow * numberOfPegsInRow;
102 | let newCodes = Array.from(codes);
103 | let newRows = Array.from(rows);
104 | let newKeys = Array.from(keys);
105 | let numberOfBlackPegs = 0;
106 | let numberOfWhitePegs = 0;
107 |
108 | // Count black pegs
109 | for (let i = 0; i < numberOfPegsInRow; i++) {
110 | const index = startIndex + i;
111 |
112 | if (newRows[index] === newCodes[i]) {
113 | numberOfBlackPegs++;
114 | delete (newRows[index]);
115 | delete (newCodes[i]);
116 | }
117 | }
118 |
119 | // Count white pegs
120 | for (let i = 0; i < numberOfPegsInRow; i++) {
121 | const index = startIndex + i;
122 | const indexOfPeg = newCodes.indexOf(newRows[index])
123 |
124 | if (indexOfPeg !== -1) {
125 | numberOfWhitePegs++;
126 | delete (newRows[index]);
127 | delete (newCodes[indexOfPeg])
128 | }
129 | }
130 |
131 | // Update key pegs
132 | for (let i = 0; i < numberOfBlackPegs; i++) {
133 | newKeys[startIndex + i] = 'black';
134 | }
135 | for (let i = numberOfBlackPegs; i < numberOfBlackPegs + numberOfWhitePegs; i++) {
136 | newKeys[startIndex + i] = 'white';
137 | }
138 | this.setState({ keys: newKeys });
139 |
140 | // Check win
141 | if (numberOfBlackPegs === numberOfPegsInRow) {
142 | this.setState({
143 | openResultDialog: true,
144 | isWin: true
145 | })
146 | } else if (currentRow === numberOfRows - 1) {
147 | this.setState({
148 | openResultDialog: true,
149 | isWin: false
150 | })
151 | } else {
152 | this.setState({
153 | currentPeg: (currentRow + 1) * numberOfPegsInRow
154 | });
155 | }
156 |
157 | // Increase step
158 | this.setState({ currentRow: currentRow + 1 })
159 | }
160 |
161 | _onCloseIntroDialog = () => {
162 | this.setState({ openIntroDialog: false })
163 | }
164 |
165 | _onCloseResultDialog = () => {
166 | this.setState({ openResultDialog: false })
167 | }
168 |
169 | _onRestartGame = () => {
170 | this.setState({ hasRestartedGame: true });
171 |
172 | this._initGame();
173 |
174 | var body = document.body;
175 | var html = document.documentElement;
176 | body.scrollTop = 0;
177 | html.scrollTop = 0;
178 | }
179 |
180 | render() {
181 | const { openIntroDialog, openResultDialog, isWin, hasRestartedGame, currentRow } = this.state
182 |
183 | return (
184 |
188 |
189 |
190 |
191 | Master Mind
192 |
193 |
194 |
195 |
196 |
203 |
204 |
211 |
212 |
216 |
217 | );
218 | }
219 | }
220 |
221 | export default App;
222 |
--------------------------------------------------------------------------------