Skip to content

Instantly share code, notes, and snippets.

@KDCinfo
Last active December 24, 2017 12:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KDCinfo/2bc1be7f4f0527a4cbbff421d4e8464b to your computer and use it in GitHub Desktop.
Save KDCinfo/2bc1be7f4f0527a4cbbff421d4e8464b to your computer and use it in GitHub Desktop.
Simon 20 React

Simon 20 (React / Responsive)

Demos and Source Code

Simon was the result of the final coding challenge for Free Code Camp's Front-End Development Certification course.

In addition to the requirements provided by the challenge, I added 3 speed levels, and an "Insights" panel (cheat mode). I then took another couple days and converted it to be responsive.

Rules

Successfully remember (or guess) up to 20 in a row to win. With 'Strict' mode turned off, you get 1 second chance if you mess up (Simon will automatically show you the way... yet again). Now go play and have fun!

Keyboard Navigation

  • Enter: Begin a new game.
  • Space: Begin a new game.
  • Escape: Begin a new game.
  • S: Toggle Strict Mode and begin a new game.
  • R | 1: Red cell.
  • B | 2: Blue cell.
  • G | 3: Green cell.
  • Y | 4: Yellow cell.

FCC Certification

Free Code Camp Front-End Development Certification Achieved (291 coding challenges completed).

Project History

// @DONE: Converted to React. (Took 2+ days.)
// @DONE: Get audio working (Took 3+ days.)
// @DONE: 2017-11-29

2017-11-20 - Mon (2 hours)
Think I put in a couple hours on Simon -> React.
	- Got overall skeleton laid out and factored.

2017-11-21 - Tue (4 hours)
Worked a bit on Simon -> React (maybe 2-4 hours)

2017-11-25 - Saturday (14 hours)
Finished rough draft of Simon JS->React conversion and pasted into CodePen.
	12-3am -- Working through snags.
	Worst snag so far is with the audio tags.

@5:35 AM
	- Still stuck on <audio> tag
		.pause() is not a function

2017-11-26 - Sunday (8 hours)
@1:20 AM - React Simon React
	- I can at least now hear the sounds when the app loads (in CodePen),
		just not when clicked.
	- Now... to gain control over them
		(so they don't play when it loads, and do play when clicked).

2017-11-27 - Monday (8 hours)
@6:45 PM
	- I refactored - 2nd time.
	- I must go the 'refs' route.
	- Now using 4 (individual) state variables to save Audio refs.
		As opposed to trying to
			1) Overwrite the initial <Audio> references, and
			2) Creating a new buttonrefs[] array (mirroring buttons[]).
	- Button sounds seem to be working.
Spent all night cleaning it up and getting it set for final.
	- Ran into multiple issues with my not doing setState correctly (from memory; from Aug; 3 months back).
	this.setState({ game: {...this.state.game, gameOver: false }, () => { console.log('done'); });

2017-11-28 - Tuesday (6 hours)
@9:30 PM
	- Spent all day on another issue with new games not starting or resetting.
	- Got it working. Problem was with multiple setStates in different functions in startIt().
	- Cleaned up even more areas that were weak.

2 + 4 + 14 + 8 + 8 + 6 = ~42 hours

I don't feel too bad for not having done React in 3 months after having just learned it.

body {
font-family: sans-serif;
margin: 5px auto 2rem;
padding: 1rem;
text-align: center;
}
h1 {
margin: 0 auto 1rem;
}
p.pojs {
font-size: 0.95rem;
margin: 0 auto 1.5rem;
}
.container {
margin: 0 auto;
position: relative;
width: 90%;
/*height: 90%;*/
min-width: 285px; /* 200px was good, but FireFox had issues */
max-width: 800px;
}
.outer {
border: 1px dotted gray;
box-shadow: 10px 10px gray;
margin: 0 auto;
position: relative;
text-align: center;
width: 100%;
}
.outer.buzz::after {
background-color: transparent;
border: 0px solid transparent;
content: '';
margin: 0 auto;
position: absolute;
top: 0;
left: 0;
z-index: 100; /* Should go above cells, but below scoreboard. */
width: 100%;
height: 100%;
}
.sq-setter-w {
width: 100%;
height: auto;
visibility: hidden;
}
.inside {
/*outline: 1px solid blue;*/
margin: 0 auto;
position: absolute;
bottom: 0;
top: 0;
left: 0;
right: 0;
z-index: 1;
min-width: 200px;
max-width: 800px;
max-height: 800px;
}
.inside2 {
/*outline: 1px solid orange;*/
font-size: 3vw;
margin: 1em auto 0;
display: inline-block;
position: relative;
height: 90%;
width: 90%;
}
/*#scoreboard {
perspective: 600px;
-webkit-perspective: 600px;
-moz-perspective: 600px;
}*/
.scoreboard {
backface-visibility: hidden;
transform: perspective(600px) rotateY(0deg);
transition: transform .5s linear 0s;
}
.scoreboard.flip {
transform: perspective(600px) rotateY(-180deg);
}
.cheat {
backface-visibility: hidden;
transform: perspective(600px) rotateY(180deg);
transition: transform .5s linear 0s;
}
.cheat.flip {
box-shadow: inset 1px 1px 5px darkgray;
/*display: inline-block;*/
transform: perspective(600px) rotateY(0deg);
z-index: 150;
}
/* Scoreboard Panel */
.scoreboard {
/*outline: 3px solid green;*/
background-color: gold;
border: 5px solid #ffffff;
border-radius: 100%;
box-shadow: inset 1px 1px 5px darkgray;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
overflow: hidden;
position: absolute;
bottom: 24%;
top: 24%;
left: 23%;
right: 23%;
height: 50%;
width: 50%;
z-index: 5;
}
.scoreboard > div {
display: inline;
}
.scoreboard-top > div,
.scoreboard-middle > div,
.scoreboard-bottom > div {
outline: 0px solid blue;
display: inline-block;
margin: 0.25em 0 0;
padding: 0;
}
.scoreboard-top input {
vertical-align: middle;
}
/* START BUTTON */
.scoreboard-top .div-start {
font-weight: bold;
margin: 0 0 0.35em;
padding: 0;
}
#start {
background-color: red;
border: 2px solid green;
border-radius: 50%;
box-shadow: 0.1em 0.1em darkgray;
cursor: pointer;
display: inline-block;
vertical-align: bottom;
width: 3vw;
height: 3vw;
max-width: 22px;
min-width: 10px;
max-height: 22px;
min-height: 10px;
}
#start:hover,
#start:active,
#start:focus {
border: 2px solid darkblue;
outline: 0;
}
#start:hover {
background-color: orangered;
/*box-shadow: 0.5em 0.5em inset white;*/
}
#simon-speed {
height: 14px;
text-align: center;
vertical-align: baseline;
width: 40px;
}
input[type="number"] {
font-size: 0.7em;
padding: 0.2em 0;
}
.scoreboard-middle {
font-size: 1.1em;
font-weight: bold;
}
.scoreboard-middle > div {
height: 2em;
margin: 0.75em auto 0.25em;
min-width: 10em;
position: relative;
text-align: center;
vertical-align: bottom;
}
.game-is {
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translate(0, -50%);
}
.scoreboard-bottom {
/*margin: 0.35em 0 0;*/
}
/*.scoreboard-bottom > div.clearb*/
div.clearb {
clear: both;
display: block;
height: 0.1em;
margin: 0 auto;
outline: 0px solid black;
width: 1px;
}
.cheat {
/*outline: 3px solid green;*/
background-color: gold;
border: 5px solid #ffffff;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: column;
overflow: hidden;
position: absolute;
bottom: 24%;
top: 24%;
left: 23%;
right: 23%;
height: 50%;
width: 50%;
z-index: 5;
}
.cheat .cheat-first {
margin: 1em 0 0.75em;
}
.cheat > div {
display: inline;
/* font-size: 2.5vw; */
margin: 0.25em 1em;
}
.cells {
display: flex;
flex-wrap: wrap;
position: relative;
height: 100%;
width: 100%;
}
.cell {
border: 2px solid #fff;
/* border-radius: 50%; */
box-shadow: 0px 0px gray;
cursor: pointer;
/*display: inline-block;*/
opacity: 0.75;
position: relative;
width: 48%;
min-width: 100px;
min-height: 100px;
}
.cell:hover,
.cell:active,
.cell:focus {
outline: 0;
opacity: 0.85;
}
.cell.buzzed {
opacity: 1;
}
#ul {
background-color: red;
border-top-left-radius: 100%;
border-top-width: 3px;
border-left-width: 3px;
border-top-color: gray;
border-left-color: gray;
}
#ur {
background-color: blue;
border-top-right-radius: 100%;
border-top-width: 3px;
border-right-width: 3px;
border-top-color: gray;
border-right-color: gray;
}
#ll {
background-color: green;
border-bottom-left-radius: 100%;
border-bottom-width: 3px;
border-left-width: 3px;
border-bottom-color: gray;
border-left-color: gray;
}
#lr {
background-color: orange;
border-bottom-right-radius: 100%;
border-bottom-width: 3px;
border-right-width: 3px;
border-bottom-color: gray;
border-right-color: gray;
}
.cell.buzz::after {
background-color: #ffffff;
border: 0.75em solid yellow;
content: ' ';
opacity: 1;
position: absolute;
top: -1em;
left: -1em;
width: 98%;
height: 98%;
/*min-width: 100px;*/
/*min-height: 100px;*/
z-index: 3;
}
#ul.buzz::after {
background-color: red;
border-top-left-radius: 95%;
}
#ur.buzz::after {
background-color: blue;
border-top-right-radius: 95%;
}
#ll.buzz::after {
background-color: green;
border-bottom-left-radius: 95%;
}
#lr.buzz::after {
background-color: orange;
border-bottom-right-radius: 95%;
}
.rules {
border: 0px dotted yellow;
margin-top: 1.5rem;
padding: 0 1rem;
text-align: left;
}
.keynav {
line-height: 1.5;
}
footer {
background-color: rgba(235, 245, 255, 1);
border-top: 1px dotted darkgray;
font-size: 11px;
margin: 0 auto;
padding: 0.25rem 0;
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
z-index: 1;
}
.hide,
.hidden {
display: none;
}
ul {
list-style: square;
}
ul.attribution {
font-size: 0.9rem;
line-height: 1.5;
list-style: square;
margin: 0 auto;
padding: 0 1rem;
text-align: left;
width: 95%;
max-width: 420px;
}
ul.fcc-links {
border: 1px dotted gray;
border-radius: 2px;
font-size: 0.9rem;
line-height: 1.5;
list-style: none;
margin: 1rem auto 0;
padding: 1rem;
text-align: left;
width: 95%;
max-width: 420px;
}
.zoom {
display: none;
margin: 0.5em 0;
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 200;
}
.zoom-0-50 {
zoom: 0.5;
-moz-transform: scale(0.5);
-moz-transform-origin: 50 0;
}
.zoom-0-75 {
zoom: 0.75;
-moz-transform: scale(0.75);
-moz-transform-origin: 50 0;
}
.outer-h-0-75 {
height: 39em;
}
.outer-h-0-50 {
height: 26em;
}
.zoom button {
cursor: pointer;
font-size: 0.9em;
}
@media screen and (min-width: 800px) {
/* .container max-width: 800px; */
.inside2 {
font-size: 20px;
}
.scoreboard-middle {
font-size: 30px;
font-weight: bold;
}
input[type="number"] {
font-size: 20px;
}
.cheat.flip > div {
font-size: 20px;
}
.zoom {
display: block;
}
.scoreboard,
.cheat {
position: absolute;
top: 25%;
}
}
// console.clear();
// import { cellConvert, gameSpeeds, ui } from './config.js'
const cellConvert = [ // Convert cell quadrants to array indexes.
'ul', // 0
'ur', // 1
'll', // 2
'lr' // 3
],
gameSpeeds = {
1: { each: 900, wait: 700 }, // Slow each; slow wait
2: { each: 600, wait: 400 }, // Default
3: { each: 300, wait: 200 }, // Quick each; quick wait
// 1 2 3
// revealSimon - setInterval - every 600 buzzIt [400 / 600 / 800]
// buzzIt - setTimeout - wait 400 then buzzOff [200 / 400 / 600]
},
ui = { keyspecs: [ // Must stay in order
{ name: 'SPACE', code: ' ', cell: 'start' },
{ name: 'ENTER', code: 'enter', cell: 'start' },
{ name: 'ESC', code: 'escape', cell: 'start' },
{ name: 's', code: 's', cell: 'simon-yolo' },
{ name: 'r', code: 'r', cell: 'ul' }, { name: '1', code: '1', cell: 'ul' },
{ name: 'b', code: 'b', cell: 'ur' }, { name: '2', code: '2', cell: 'ur' },
{ name: 'g', code: 'g', cell: 'll' }, { name: '3', code: '3', cell: 'll' },
{ name: 'y', code: 'y', cell: 'lr' }, { name: '4', code: '4', cell: 'lr' }
] };
class Simon extends React.Component {
constructor(props) {
super(props);
this.state = {
game: {
whoClicked: 's', // 's'|'p' (Simon or Player) - A controlled variable to be handled each click.
yolo: false, // Strict Mode (no 2nd chance after a wrong click).
hasTried: 0, // 0|1 - Allowed 1 retry (if yolo isn't true).
playerArray: [], // Cells the player has clicked.
simonArray: [], // Cells the game has chosen (added post-click).
winArray: [], // [3,2,5,6,1,...] Number of successful clicks in previous games.
gameOver: true,
gameSpeed: 2, // [1-3] Slow, Medium*, Fast
gameFlipped: false, // Used in both 'scoreboard' and 'cheat' panels (which side shows).
gameText: 'Ready', // Main verbiage in middle of scoreboard.
},
buttons: ['', '', '', ''],
buttonref0: '',
buttonref1: '',
buttonref2: '',
buttonref3: '',
insideDiv: ['inside'],
outerDiv: ['outer'],
cellClassName0: ['cell'],
cellClassName1: ['cell'],
cellClassName2: ['cell'],
cellClassName3: ['cell'],
};
this.handleKeyPress = this.handleKeyPress.bind(this);
this.disableCells = this.disableCells.bind(this);
// this.resetCells = this.resetCells.bind(this);
this.setButtons = this.setButtons.bind(this);
this.setGameSpeed = this.setGameSpeed.bind(this);
this.setGameText = this.setGameText.bind(this);
this.setSimonFlip = this.setSimonFlip.bind(this);
this.setSimonYolo = this.setSimonYolo.bind(this);
this.setZoom = this.setZoom.bind(this);
this.setCell = this.setCell.bind(this);
this.generateNextCell = this.generateNextCell.bind(this);
this.enableCells = this.enableCells.bind(this);
this.revealSimon = this.revealSimon.bind(this);
this.buzzCell = this.buzzCell.bind(this);
this.startIt = this.startIt.bind(this);
this.buzzIt = this.buzzIt.bind(this);
this.buzzOn = this.buzzOn.bind(this);
this.buzzOff = this.buzzOff.bind(this);
}
componentWillMount() {
document.addEventListener('keydown', this.handleKeyPress);
// { this.state.buttons.map( (elem, idx) => {
// let thisAudio = <Audio key={idx} idx={idx} />,
// { buttons } = this.state,
// newButtons = buttons.slice(0);
// newButtons[idx] = thisAudio;
// } ) }
let newButtons = this.state.buttons.slice(0);
for (var idx = 0; idx < 4; idx++) {
let thisAudio = <Audio key={idx} idx={idx} thisCell={this.state.buttons[idx]} setCell={this.setCell} />; // ref={'child' + idx} | this.refs.child.getAlert()
newButtons[idx] = thisAudio;
}
this.setState({ buttons: newButtons }, () => {
});
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeyPress);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyPress);
}
handleKeyPress(e) {
// React handleKeyPress -- Thanks to: https://codepen.io/anon/pen/ZOzaPW/
// [event.keyCode] is deprecated.
// [event.key] is its replacement.
// [event.code] is not supported well (2017-11-07).
let eKey = e.key.toLowerCase(); // / * - + Enter . Space
if (eKey === ui.keyspecs[0].code || // SPACE
eKey === ui.keyspecs[1].code || // ENTER
eKey === ui.keyspecs[2].code) { // ESCAPE
e.preventDefault();
this.startIt();
}
if (eKey === ui.keyspecs[3].code) { // S (strict mode toggle)
e.preventDefault();
this.setState({ game: {...this.state.game, 'yolo': (this.state.game.yolo ? false : true) } }, () => {
this.startIt();
});
}
if (eKey === ui.keyspecs[4].code || eKey === ui.keyspecs[5].code) { // Red or 1
e.preventDefault();
this.disableCells();
this.buzzCell(cellConvert.indexOf('ul'));
}
if (eKey === ui.keyspecs[6].code || eKey === ui.keyspecs[7].code) { // Blue or 2
e.preventDefault();
this.disableCells();
this.buzzCell(cellConvert.indexOf('ur'));
}
if (eKey === ui.keyspecs[8].code || eKey === ui.keyspecs[9].code) { // Green or 3
e.preventDefault();
this.disableCells();
this.buzzCell(cellConvert.indexOf('ll'));
}
if (eKey === ui.keyspecs[10].code || eKey === ui.keyspecs[11].code) { // Yellow or 4
e.preventDefault();
this.disableCells();
this.buzzCell(cellConvert.indexOf('lr'));
}
}
// 3 Aesthetic Functions to Highlight Cells
buzzIt(cellId) { // cellId = [0-3]
this.buzzOn(cellId);
setTimeout( () => {
this.buzzOff(cellId);
}, gameSpeeds[this.state.game.gameSpeed].wait); // Show cell for partial second
// gameSpeeds = { 1: { each: 400, wait: 200 },
}
buzzOn(cellId) {
let whichCell = 'cellClassName' + cellId,
buttonref = 'buttonref' + cellId,
newClassList = this.state[whichCell].slice(0);
newClassList.push('buzz', 'buzzed');
this.setState({ [whichCell]: newClassList });
// Sound /* id = simon-audio-[0-3] */
// Pausing and setting currentTime helps fix non-playing overlaps on fast mode.
// Thanks to: https://stackoverflow.com/a/32041746/638153
this.state[buttonref].pause();
this.state[buttonref].currentTime = 0;
this.state[buttonref].play();
}
buzzOff(cellId) {
let whichCell = 'cellClassName' + cellId, // cellClassName[0-3]
newClassList = this.state[whichCell].slice(0), // Copy of specific cell's array of class names.
indexBuzz = newClassList.indexOf('buzz'), // If class name 'buzz' exists, remove it.
indexBuzzed = -1;
if (indexBuzz >= 0) {
newClassList.splice(indexBuzz, 1);
}
indexBuzzed = newClassList.indexOf('buzzed'); // If class name 'buzzed' exists, remove it.
if (indexBuzzed >= 0) {
newClassList.splice(indexBuzzed, 1);
}
this.setState({ [whichCell]: newClassList });
}
// Other Upper-Level Functions
disableCells() {
const outerDivClass = this.state.outerDiv.slice(0);
outerDivClass.push('buzz');
this.setState({ outerDiv: outerDivClass });
}
// resetCells() {
// this.setState({ game: {...this.state.game, playerArray: [] } }, () => {
// this.setState({ game: {...this.state.game, simonArray: [] } }, () => {
// // this.updateDisplays(); // All fields are now reactive
// });
// });
// }
setButtons(audioArray) {
let tmpButtonsArray = this.state.buttons.slice(0);
audioArray.map( (tmpBtn, idx) => {
tmpButtonsArray[idx] = tmpBtn;
});
this.setState({ buttons: tmpButtonsArray });
}
setGameSpeed(newSpeed) {
this.setState({ game: {...this.state.game, gameSpeed: newSpeed } });
}
setSimonFlip() {
this.setState({ game: {...this.state.game, gameFlipped: !this.state.game.gameFlipped } });
}
setSimonYolo() {
this.setState({ game: {...this.state.game, yolo: (this.state.game.yolo ? false : true) } }, () => {
this.startIt();
});
}
setGameText(newVal) {
this.setState({ game: {...this.state.game, 'gameText': newVal } }, () => {
});
}
startIt() {
// document.getElementById('start').addEventListener('click', function(e) { });
this.disableCells();
this.setState({ game: {
...this.state.game,
whoClicked: 's', // this.setWhoClick('s')
gameOver: false, // this.setGameOver(false)
hasTried: 0, // this.setHasTried(0)
gameText: 'In Play', // this.setGameText('In Play')
playerArray: [], // this.resetCells()
simonArray: [] // this.resetCells()
}}, () => {
// Issue with [space/enter] startIt() resetting and starting a new game,
// but neither 'S' nor either click ('Strict' or 'Start') would start a new game.
// Issue was with multiple (async) setStates being done in separate function calls called from startIt().
// Pulled each setState from their respective function calls and put directly into the startIt() function,
// and put them all in the same setState action; as they should be (including resetCells).
this.generateNextCell();
});
}
generateNextCell() {
// Wait 3/4 of a sec, then buzz the cell with [0-3].
setTimeout( () => { this.buzzCell(this.getRandomCell()) }, 750);
}
enableCells() {
const outerDivClass = this.state.outerDiv.slice(0);
outerDivClass.splice(outerDivClass.indexOf('buzz'), 1);
this.setState({ outerDiv: outerDivClass });
// Array.from(document.getElementsByClassName('outer'))[0].classList.remove('buzz');
}
getRandomCell() {
return Math.floor(Math.random() * 4);
}
revealSimon() {
var i = 0,
{ playerArray } = this.state.game;
var showThem = setInterval( () => {
if (i < this.state.game.simonArray.length) {
// Grab each index from the 'simonArray', and convert it to a cell ID.
this.buzzIt(this.state.game.simonArray[i]);
} else {
clearInterval(showThem);
// document.getElementById('game-over').innerText = 'Waiting...\r\n(it\'s your turn)';
// this.state.game.playerArray = [];
this.setState({
game: {
...this.state.game,
gameOver: false,
gameText: 'Waiting...\r\n(it\'s your turn)',
playerArray: []
}
}, () => {
this.enableCells();
});
}
i++;
// console.log('revealSimon: ', this.state.game.gameSpeed);
// console.log('setInterval: ', gameSpeeds[this.state.game.gameSpeed].each);
}, gameSpeeds[this.state.game.gameSpeed].each);
// gameSpeeds = {
// 1: { each: 400, wait: 200 },
}
buzzCell(cell) {
// This is the 'brains' of the app (i.e., the Controller).
// cell = number [0-3]
var cellId = cellConvert[cell], // cellId = 'ul'|'ur'|'ll'|'lr'
turnSimon = this.state.game.whoClicked === 's',
gameOver = this.state.game.gameOver; // && this.state.game.gameText.toLowerCase() !== 'ready'
// console.log('buzzCell: ', cell, cellId, turnSimon, gameOver); // 3, lr, true, false
if (gameOver) {
this.buzzIt(cell); // Just for fun... game is over.
setTimeout( () => this.enableCells(), 100);
} else if (turnSimon) {
let { simonArray, whoClicked } = this.state.game,
simonArrayNew = this.state.game.simonArray.slice(0);
simonArrayNew.push(cell);
this.setState({ game: {...this.state.game, simonArray: simonArrayNew } }, () => {
// game.simonArray.push(cell); // [0,1,2,3,0,1,2,3]
// updatePlayCountDisplay(); // Should be run whenever simonArray is changed.
// Show all of Simon's moves.
this.revealSimon();
// Swap side back to player.
// game.whoClicked = 'p'; // Control the var!
this.setState({ game: {...this.state.game, whoClicked: 'p' } });
});
} else {
// Player's Turn
let { playerArray, gameOver, gameText, hasTried, whoClicked, winArray } = this.state.game,
playerArrayNew = this.state.game.playerArray.slice(0),
winArrayNew = this.state.game.winArray.slice(0);
// Store it.
// game.playerArray.push(cell); // [] ... [3]
playerArrayNew.push(cell);
// console.log('buzzCell ELSE: ', cell, cellId, turnSimon, gameOver);
this.setState({ game: {...this.state.game, playerArray: playerArrayNew } }, () => {
// Buzz it.
this.buzzIt(cell);
let tempCheck = false;
// Then test them all.
for (var i = 0; i < this.state.game.playerArray.length; i++) {
if (this.state.game.playerArray[i] !== this.state.game.simonArray[i]) {
// Incorrect - Reset
this.setState({ game: {...this.state.game, playerArray: [] } });
tempCheck = true;
// game.playerArray = [];
break;
}
}
if (tempCheck) { // this.state.game.playerArray.length === 0 // State isn't set yet (async)...
// An incorrect cell was clicked (and playerArray was zeroed out).
if (this.state.game.hasTried === 0 && !this.state.game.yolo) {
// document.getElementById('game-over').innerText = '...whoops!!!';
// game.hasTried = 1;
this.setState({ game: {
...this.state.game,
gameText: '...whoops!!!',
hasTried: 1
} }, () => {
this.revealSimon();
});
} else {
winArrayNew.push(this.state.game.simonArray.length);
this.setState({ game: {
...this.state.game,
gameOver: true,
gameText: 'Lost',
hasTried: 0,
whoClicked: 's',
winArray: winArrayNew,
playerArray: [],
simonArray: []
} }, () => {
this.enableCells();
// this.resetCells();
});
// game.gameOver = true;
// document.getElementById('game-over').innerText = 'Lost';
// game.hasTried = 0;
// game.whoClicked = 's'; // Control the var!
// game.winArray.push(game.simonArray.length);
}
} else if (this.state.game.playerArray.length === 20) {
// You Won!!!
winArrayNew.push(20);
this.setState({ game: {
...this.state.game,
gameOver: true,
gameText: 'Won!!!',
hasTried: 0,
winArray: winArrayNew
} }, () => {
this.enableCells();
});
// game.gameOver = true;
// document.getElementById('game-over').innerText = 'Won!!!';
// game.hasTried = 0;
// game.winArray.push(20);
} else if (this.state.game.playerArray.length === this.state.game.simonArray.length) {
// You survived another round!!!
this.setState({ game: {
...this.state.game,
gameText: 'In Play',
hasTried: 0,
whoClicked: 's',
playerArray: []
} }, () => {
this.generateNextCell();
});
// document.getElementById('game-over').innerText = 'In Play';
// game.hasTried = 0;
// game.whoClicked = 's'; // Control the var!
// game.playerArray = [];
} else {
this.enableCells();
}
});
}
}
setCell(cellId, cellRef) {
let newButtonRef = 'buttonref' + cellId;
this.setState({ [newButtonRef]: cellRef });
}
setZoom(level) {
let insideDivNew = this.state.insideDiv.slice(0),
outerDivNew = this.state.outerDiv.slice(0);
// if (0-50) Remove 0-75, Add 0-50 | if (0-75) Remove 0-50, Add 0-75 | else; Remove both
if (level === '0-50') {
// console.log('here 0-50');
let insideDivHas75 = insideDivNew.indexOf('zoom-0-75'),
outerDivHas75 = outerDivNew.indexOf('outer-h-0-75');
if (insideDivHas75 >= 0) { insideDivNew.splice(insideDivHas75, 1); }
if (outerDivHas75 >= 0) { outerDivNew.splice(outerDivHas75, 1); }
insideDivNew.push('zoom-0-50');
outerDivNew.push('outer-h-0-50')
this.setState({ insideDiv: insideDivNew });
this.setState({ outerDiv: outerDivNew });
} else if (level === '0-75') {
// console.log('here 0-75');
let insideDivHas50 = insideDivNew.indexOf('zoom-0-50'),
outerDivHas50 = outerDivNew.indexOf('outer-h-0-50');
if (insideDivHas50 >= 0) { insideDivNew.splice(insideDivHas50, 1); }
if (outerDivHas50 >= 0) { outerDivNew.splice(outerDivHas50, 1); }
insideDivNew.push('zoom-0-75');
outerDivNew.push('outer-h-0-75')
this.setState({ insideDiv: insideDivNew });
this.setState({ outerDiv: outerDivNew });
} else { // '1-00'
// console.log('here 1-00');
let insideDivHas50 = insideDivNew.indexOf('zoom-0-50'),
outerDivHas50 = outerDivNew.indexOf('outer-h-0-50'),
insideDivHas75 = -1,
outerDivHas75 = -1;
if (insideDivHas50 >= 0) { insideDivNew.splice(insideDivHas50, 1); }
insideDivHas75 = insideDivNew.indexOf('zoom-0-75');
if (insideDivHas75 >= 0) { insideDivNew.splice(insideDivHas75, 1); }
if (outerDivHas50 >= 0) { outerDivNew.splice(outerDivHas50, 1); }
outerDivHas75 = outerDivNew.indexOf('outer-h-0-75');
if (outerDivHas75 >= 0) { outerDivNew.splice(outerDivHas75, 1); }
this.setState({ insideDiv: insideDivNew });
this.setState({ outerDiv: outerDivNew });
}
}
render() {
return (
<main>
<div className="hidden">
<div>whoClicked: {this.state.game.whoClicked}</div>
<div>yolo: {this.state.game.yolo ? 'true' : 'false'}</div>
<div>hasTried: {this.state.game.hasTried}</div>
<div>playerArray: {this.state.game.playerArray}</div>
<div>simonArray: {this.state.game.simonArray}</div>
<div>winArray: {this.state.game.winArray}</div>
<div>gameOver: {this.state.game.gameOver ? 'true' : 'false'}</div>
<div>gameSpeed: {this.state.game.gameSpeed}</div>
<div>gameFlipped: {this.state.game.gameFlipped ? 'true' : 'false'}</div>
<div>gameText: {this.state.game.gameText}</div>
</div>
<div className="container">
<div className={this.state.outerDiv.toString().replace(/\,/g, ' ')}>{/* outer */}
<img src="https://dummyimage.com/50x50/000/fff.gif&text=50x50" className="sq-setter-w" />
{/* @TODO: Bring <img> down local */}
<div className={this.state.insideDiv.toString().replace(/\,/g, ' ')}>{/* inside */}
<div className="inside2">
<Scoreboard
startIt={this.startIt}
setGameSpeed={this.setGameSpeed}
setSimonFlip={this.setSimonFlip}
setSimonYolo={this.setSimonYolo}
simonFlip={this.state.game.gameFlipped}
gameSpeed={this.state.game.gameSpeed}
gameText={this.state.game.gameText}
simonYolo={this.state.game.yolo}
playCount={this.state.game.simonArray.length}
winArraySum={this.state.game.winArray.reduce((sum, score) => { return (score === 20) ? sum + 1 : sum; }, 0)}
/>
<Cheat
setSimonFlip={this.setSimonFlip}
simonFlip={this.state.game.gameFlipped}
simonPlays={this.state.game.simonArray.map( cell => { return cell === 0 ? ' R' : cell === 1 ? ' B' : cell === 2 ? ' G' : ' Y'; } )}
playerPlays={this.state.game.playerArray.map( cell => { return cell === 0 ? ' R' : cell === 1 ? ' B' : cell === 2 ? ' G' : ' Y'; } )}
gamesPrevious={JSON.stringify(this.state.game.winArray)}
/>
<Cells
disableCells={this.disableCells}
buzzCell={this.buzzCell}
cellClassName0={this.state.cellClassName0}
cellClassName1={this.state.cellClassName1}
cellClassName2={this.state.cellClassName2}
cellClassName3={this.state.cellClassName3}
/>
</div>
</div>
<Zoom setZoom={this.setZoom} />
</div>
<Rules />
<div>{ this.state.buttons.map( (elem, idx) => {
// elem.ref = 'child' + idx; // Read-only
// console.log('Simon map: elem: ', elem);
return elem;
} ) }</div>
</div>
</main>
);
}
}
// END OF MAIN SIMON COMPONENT CLASS
class Scoreboard extends React.Component {
constructor(props) {
super(props);
let currentGameSpeed = this.props.gameSpeed;
this.state = {
gameSpeed: currentGameSpeed,
};
this.simonSpeed = this.simonSpeed.bind(this);
this.propStartIt = this.propStartIt.bind(this);
}
componentDidMount() {
this.refs.autoFocus.focus();
}
propStartIt(e) {
this.props.startIt();
}
simonSpeed(e) {
let newSpeed = (e.target.value === "1") ? 1 : (e.target.value === "3") ? 3 : 2;
this.setState({ gameSpeed: newSpeed }, () => {
this.props.setGameSpeed(this.state.gameSpeed);
});
}
render() {
return (
<div className={'scoreboard' + (this.props.simonFlip ? ' flip' : '')}>
<div className="scoreboard-top">
<div className="div-start">Start: <div id="start" tabIndex="0" onClick={this.propStartIt} ref="autoFocus"></div></div>
<div className="clearb"></div>
<div>
<span title="In Strict mode, no 2nd chances if you mess up.">Strict:
<input id="simon-yolo" onChange={this.props.setSimonYolo} type="checkbox" checked={this.props.simonYolo} />
</span>&nbsp;
<span title="Turn on cheat mode">Insights: <input id="simon-cheat" type="checkbox" onChange={this.props.setSimonFlip} checked={this.props.simonFlip} /></span>
</div>
<div className="clearb"></div>
<div>Speed <small>(1-3)</small>: <input id="simon-speed" type="number" min="1" max="3" onChange={this.simonSpeed} value={this.state.gameSpeed} /></div>
</div>
<div className="scoreboard-middle">
<div><div className="game-is"><span>Game is: <span id="game-over">{this.props.gameText}</span></span></div></div>
</div>
<div className="scoreboard-bottom">
<div>Play Count: <span id="play-count">{this.props.playCount}</span></div>
<div className="clearb"></div>
<div>Wins: <span id="game-wins">{this.props.winArraySum}</span></div>
</div>
</div>
)
}
}
class Cheat extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className={'cheat' + (this.props.simonFlip ? ' flip' : '')}>
<div className="cheat-first" title="Turn off cheat mode">Insights: <input id="simon-cheat-reverse" type="checkbox" onChange={this.props.setSimonFlip} checked={this.props.simonFlip} /></div>
<div>Simon's Plays: <span id="simon-plays">{this.props.simonPlays}</span></div>
<div>Player's Steps: <span id="player-plays">{this.props.playerPlays}</span></div>
<div>Previous Scores: <span id="games-previous">{this.props.gamesPrevious}</span></div>
</div>
)
}
}
class Cells extends React.Component {
constructor(props) {
super(props);
this.registerClick = this.registerClick.bind(this);
}
registerClick(e) {
this.props.disableCells();
// [cellConvert] Maps the cell ID ('ul') to its index [0-3].
this.props.buzzCell(cellConvert.indexOf(e.target.id));
}
render() {
return (
<div className="cells">
<div id="ul" className={this.props.cellClassName0.toString().replace(/\,/g, ' ')} tabIndex="0" onClick={this.registerClick}></div>
<div id="ur" className={this.props.cellClassName1.toString().replace(/\,/g, ' ')} tabIndex="0" onClick={this.registerClick}></div>
<div id="ll" className={this.props.cellClassName2.toString().replace(/\,/g, ' ')} tabIndex="0" onClick={this.registerClick}></div>
<div id="lr" className={this.props.cellClassName3.toString().replace(/\,/g, ' ')} tabIndex="0" onClick={this.registerClick}></div>
</div>
)
}
}
class Zoom extends React.Component {
constructor(props) {
super(props);
this.setTheZoom = this.setTheZoom.bind(this);
}
setTheZoom(e) {
this.props.setZoom(e.target.className.substring(11));
}
render() {
return (
<div className="zoom">
<button className="click-zoom-1-00" onClick={this.setTheZoom}>Zoom Full</button>&nbsp;
<button className="click-zoom-0-75" onClick={this.setTheZoom}>Zoom 3/4</button>
<button className="click-zoom-0-50" onClick={this.setTheZoom}>Zoom 1/2</button>&nbsp;
</div>
)
}
}
class Rules extends React.Component {
render() {
return(
<div>
<div className="rules">
<b>Rules:</b>
Successfully remember (or guess) up to 20 in a row to win.
With 'Strict' mode turned off, you get 1 second chance if you mess up
(Simon will automatically show you the way... <i>yet again</i>).
Now go play and have fun!
</div>
<div className="rules keynav" tabIndex="0">
<b><u>Keyboard Navigation:</u></b><br/>
<b>Enter</b>: Begin a new game.<br/>
<b>Space</b>: Begin a new game.<br/>
<b>Escape</b>: Begin a new game.<br/>
<b>S</b>: Toggle Strict Mode and begin a new game.<br/>
<b>R</b> | <b>1</b>: Red cell.<br/>
<b>B</b> | <b>2</b>: Blue cell.<br/>
<b>G</b> | <b>3</b>: Green cell.<br/>
<b>Y</b> | <b>4</b>: Yellow cell.<br/>
</div>
<div className="rules">
<b>Attribution:</b>
<ul>
<li>Learn More about <a href="https://en.wikipedia.org/wiki/Simon_(game)" target="kdcNewWin">Simon the Game</a>.</li>
<li><a href="https://www.hasbro.com/en-us/product/simon-game:6B0A06E3-5056-9047-F532-6A891FAEBA15" target="kdcNewWin">Buy Simon</a> from Hasbro.</li>
</ul>
</div>
</div>
);
}
}
class Audio extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
// AUDIO HANDLING
// each 'thisCell' (buttons[] array entry) begins empty with ''
if (this.props.thisCell !== this.refs['child' + this.props.idx]) { // this.props.thisCell.length > 0 &&
this.props.setCell(this.props.idx, this.refs['child' + this.props.idx]);
}
}
render() {
let tmpAudio = {},
{ idx } = this.props; // idx = [0-3]
tmpAudio.id = 'simon-audio-' + idx;
tmpAudio.src = 'https://s3.amazonaws.com/freecodecamp/simonSound' + (idx + 1) + '.mp3';
return (
<audio id={tmpAudio.id} src={tmpAudio.src} preload="auto" autoPlay={false} type="audio/mpeg" ref={'child' + idx}></audio>
)
}
}
ReactDOM.render(
<Simon />,
root
)
<!--
// Simon 20 // Keith D Commiskey // 2017-11-29
//
// * [CodePen: React + TypeScript](https://codepen.io/KeithDC/full/XVNgQr)
// * [Gist: React + TypeScript](https://gist.github.com/KDCinfo/c1a65aa240c8653052f092a7d74882bb)
//
// * [CodePen: React](https://codepen.io/KeithDC/full/KyoQQE)
// * [Gist: React](https://gist.github.com/KDCinfo/2bc1be7f4f0527a4cbbff421d4e8464b)
//
// * [CodePen: JavaScript](https://codepen.io/KeithDC/full/dZJoVm)
// * [Gist: JavaScript](https://gist.github.com/KDCinfo/271d88aa7630559a29a9402eb16d6208)
//
// Simon is the result of the final coding challenge for
// [Free Code Camp's Front-End Development Certification course](https://www.freecodecamp.org).
//
// Conversion took 5 days (3 of which were spent getting the audio to work).
//
-->
<h1>Simon 20 (React / Responsive)</h1>
<p class="pojs">
<a href="https://codepen.io/KeithDC/pen/XVNgQr">React + TypeScript Version</a><br/><br/>
<a href="https://codepen.io/KeithDC/pen/dZJoVm">JavaScript Version</a>
</p>
<div id="root"></div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment