Skip to content

Instantly share code, notes, and snippets.

@KDCinfo
Last active December 25, 2017 08:11
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/c1a65aa240c8653052f092a7d74882bb to your computer and use it in GitHub Desktop.
Save KDCinfo/c1a65aa240c8653052f092a7d74882bb to your computer and use it in GitHub Desktop.
Simon 20: React + TypeScript + Responsive

Simon 20 (React + TypeScript / 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.
  • 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.

Removed 'Space(bar)' to allow for default accessibility on checkboxes.

FCC Certification

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

Project History

2017-12-19 - Tuesday

  **Tue Night** - Created and setup project (local create-react-app).

2017-12-20 - Wednesday

  **Wed** - Stuck on events ? Wrapping my head between: HTMLDivElement, HTMLInputElement, HTMLButtonElement, and HTMLAudioElement

2017-12-21 - Thursday

  **Thu** - Computer maint. day, but some progress

2017-12-22 - Friday

  **Fri** - Stuck on Audio.

  **Fri Night** - Got compiler working. Ported over CSS. Looks good, but doesn't work yet.

2017-12-23 - Saturday

  **Sat** - Got 1 button working, then got all buttons working. Fixed all linting. Stuck on Refs (string => callbacks).

  **Sat Night** - Done

2017-12-24 - Sunday

  **Sun** - Logistics; Updated CodePen, created new Gist, updated previous 2 CodePens and Gists.

Since my first HTML page in 1996 (Mosaic browser), I have learned that there is always something new to figure out...

.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 80px;
}
.App-header {
background-color: #222;
height: 150px;
padding: 20px;
color: white;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
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();
/*
* INTERFACES
*/
interface GameState {
whoClicked: string;
yolo: boolean;
hasTried: number;
playerArray: number[];
simonArray: number[];
winArray: number[];
gameOver: boolean;
gameSpeed: number;
gameFlipped: boolean;
gameText: string;
}
interface SimonState {
game: GameState;
buttons: AudioProps[];
buttonref0: string;
buttonref1: string;
buttonref2: string;
buttonref3: string;
insideDiv: string[];
outerDiv: string[];
cellClassName0: string[];
cellClassName1: string[];
cellClassName2: string[];
cellClassName3: string[];
}
interface ScoreboardState {
gameSpeed: number; // currentGameSpeed
}
interface ScoreboardProps {
startIt: () => void;
setGameSpeed: (newSpeed: number) => void;
setSimonFlip: () => void;
setSimonYolo: () => void;
simonFlip: boolean;
gameSpeed: number;
gameText: string;
simonYolo: boolean;
playCount: number;
winArraySum: number;
}
interface CheatProps {
setSimonFlip: () => void;
simonFlip: boolean;
simonPlays: string[];
playerPlays: string[];
gamesPrevious: string;
}
interface CellsProps {
disableCells: () => void;
buzzCell: (cell: number) => void;
cellClassName0: string[];
cellClassName1: string[];
cellClassName2: string[];
cellClassName3: string[];
}
interface ZoomProps {
setZoom: (level: string) => void;
}
interface AudioProps {
key: number;
idx: number;
// thisCell: HTMLAudioElement; // HTMLDivElement / HTMLAudioElement / HTMLElement
setCell: (cellId: number, cellRef?: HTMLAudioElement) => void;
}
// 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'}, // Removed: To allow for toggle on checkboxes (default behavior)
{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<{}, SimonState> {
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: [], // interface SimonState { buttons: AudioProps[]; }
// interface AudioProps { setCell: (cellId: number, cellRef?: HTMLAudioElement) => void; }
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.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);
var buttons = Array.from(Array(4)); // Element
for (var idx = 0; idx < 4; idx++) {
buttons.push(<Audio key={idx} idx={idx} setCell={this.setCell} />);
}
this.setState({ buttons });
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeyPress);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyPress);
}
handleKeyPress(evt: KeyboardEvent) {
// KeyboardEvent<HTMLElement>
// 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 = evt.key.toLowerCase(); // / * - + Enter . Space
if (//eKey === ui.keyspecs[0].code || // SPACE
eKey === ui.keyspecs[1].code || // ENTER
eKey === ui.keyspecs[2].code) { // ESCAPE
evt.preventDefault();
this.startIt();
}
if (eKey === ui.keyspecs[3].code) { // S (strict mode toggle)
evt.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
evt.preventDefault();
this.disableCells();
this.buzzCell(cellConvert.indexOf('ul'));
}
if (eKey === ui.keyspecs[6].code || eKey === ui.keyspecs[7].code) { // Blue or 2
evt.preventDefault();
this.disableCells();
this.buzzCell(cellConvert.indexOf('ur'));
}
if (eKey === ui.keyspecs[8].code || eKey === ui.keyspecs[9].code) { // Green or 3
evt.preventDefault();
this.disableCells();
this.buzzCell(cellConvert.indexOf('ll'));
}
if (eKey === ui.keyspecs[10].code || eKey === ui.keyspecs[11].code) { // Yellow or 4
evt.preventDefault();
this.disableCells();
this.buzzCell(cellConvert.indexOf('lr'));
}
}
// 3 Aesthetic Functions to Highlight Cells
buzzIt(cellId: number) { // 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: number) {
let whichCell = 'cellClassName' + cellId.toString(),
buttonref = 'buttonref' + cellId.toString(),
newClassList = this.state[whichCell].slice(0),
tmpObj = {};
newClassList.push('buzz', 'buzzed');
tmpObj[whichCell] = newClassList;
this.setState( tmpObj );
// this.setState({ [whichCell]: newClassList }); // game
// 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: number) {
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,
tmpObj = {};
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);
}
tmpObj[whichCell] = newClassList;
this.setState( tmpObj );
}
// Other Upper-Level Functions
disableCells() {
const outerDivClass = this.state.outerDiv.slice(0);
outerDivClass.push('buzz');
this.setState({ outerDiv: outerDivClass });
}
setGameSpeed(newSpeed: number) {
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: string) {
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;
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: number) {
// 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: []
}
},
(): void => {
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: number, cellRef?: HTMLAudioElement) {
// Per: https://stackoverflow.com/a/29281499/638153
let tmpRef = {};
tmpRef['buttonref' + cellId.toString()] = cellRef;
this.setState(tmpRef);
// Untested fallback
// let newButtonRef = 'buttonref' + cellId.toString();
// this.setState({ ...this.state, [newButtonRef]: cellRef });
}
setZoom(level: string) {
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>
{/* Add the HTML Audio Tags that were added to buttons[] in `componentWillMount` */}
{this.state.buttons.map( tag => tag )}
</div>
</div>
</main>
);
}
}
// END OF MAIN SIMON COMPONENT CLASS
class Scoreboard extends React.Component<ScoreboardProps, ScoreboardState> {
constructor(props: ScoreboardProps) {
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.divFocus.focus();
}
propStartIt(e: React.SyntheticEvent<HTMLButtonElement>) {
e.preventDefault();
this.props.startIt();
}
simonSpeed(e: React.SyntheticEvent<HTMLInputElement>) {
let newSpeed = (e.currentTarget.value === '1') ? 1 : (e.currentTarget.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:
<button type="button" id="start" onClick={this.propStartIt} autoFocus={true} />
</div>
<div className="clearb" />
<div>
<span title="In Strict mode, no 2nd chances if you mess up.">Strict:
<input
type="checkbox"
id="simon-yolo"
onChange={this.props.setSimonYolo}
checked={this.props.simonYolo}
/>
</span>&nbsp;
<span title="Turn on cheat mode">Insights:
<input
type="checkbox"
id="simon-cheat"
onChange={this.props.setSimonFlip}
checked={this.props.simonFlip}
/>
</span>
</div>
<div className="clearb" />
<div>Speed <small>(1-3)</small>:
<input
type="number"
id="simon-speed"
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>Wins: <span id="game-wins">{this.props.winArraySum}</span></div>
</div>
</div>
);
}
}
class Cheat extends React.Component<CheatProps, {}> {
constructor(props: CheatProps) {
super(props);
}
render() {
return (
<div className={'cheat' + (this.props.simonFlip ? ' flip' : '')}>
<div className="cheat-first" title="Turn off cheat mode">Insights:
<input
type="checkbox"
id="simon-cheat-reverse"
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<CellsProps, {}> {
constructor(props: CellsProps) {
super(props);
this.registerClick = this.registerClick.bind(this);
}
registerClick(e: React.SyntheticEvent<HTMLDivElement>) {
this.props.disableCells();
// [cellConvert] Maps the cell ID ('ul') to its index [0-3].
this.props.buzzCell(cellConvert.indexOf(e.currentTarget.id));
}
render() {
return (
<div className="cells">
<div
id="ul"
className={this.props.cellClassName0.toString().replace(/\,/g, ' ')}
tabIndex={0}
onClick={this.registerClick}
/>
<div
id="ur"
className={this.props.cellClassName1.toString().replace(/\,/g, ' ')}
tabIndex={0}
onClick={this.registerClick}
/>
<div
id="ll"
className={this.props.cellClassName2.toString().replace(/\,/g, ' ')}
tabIndex={0}
onClick={this.registerClick}
/>
<div
id="lr"
className={this.props.cellClassName3.toString().replace(/\,/g, ' ')}
tabIndex={0}
onClick={this.registerClick}
/>
</div>
);
}
}
class Zoom extends React.Component<ZoomProps, {}> {
constructor(props: ZoomProps) {
super(props);
this.setTheZoom = this.setTheZoom.bind(this);
}
setTheZoom(e: React.SyntheticEvent<HTMLButtonElement>) {
this.props.setZoom(e.currentTarget.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>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<AudioProps, {}> {
refs: {
[key: string]: (HTMLAudioElement)
'child0': (HTMLAudioElement) // !important <Audio
'child1': (HTMLAudioElement)
'child2': (HTMLAudioElement)
'child3': (HTMLAudioElement)
};
constructor(props: AudioProps) {
super(props);
}
componentDidMount() {
// AUDIO HANDLING
var t1 = 'child' + this.props.idx.toString();
// each 'thisCell' (buttons[] array entry) begins empty
if (this.refs['child' + this.props.idx]) {
this.props.setCell(this.props.idx, this.refs[t1]);
}
}
render() {
let tmpAudio = {id: '', src: ''},
{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}
itemType="audio/mpeg"
ref={'child' + idx.toString()}
/>
);
}
}
ReactDOM.render(
<Simon />,
root
)
<!--
// Simon 20 React + TypeScript // Keith D Commiskey // 2017-12-24
//
// * [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 4+ days.
//
-->
<h1>Simon 20 (React + TypeScript / Responsive)</h1>
<p class="pojs">
<a href="https://codepen.io/KeithDC/pen/KyoQQE">React Version</a><br/><br/>
<a href="https://codepen.io/KeithDC/pen/dZJoVm">JavaScript Version</a>
</p>
<div id="root"></div>
<div class="rules history">
<b>Development History:</b> <span>2017-12-[19-23]</span>
<ul class="listitems">
<li><b>Tue Night</b> - Created and setup project (local create-react-app).</li>
<li><b>Wed</b> - Stuck on events ? Wrapping my head between:
HTMLDivElement, HTMLInputElement, HTMLButtonElement, and HTMLAudioElement</li>
<li><b>Thu</b> - Computer maint. day, but some progress</li>
<li><b>Fri</b> - Stuck on Audio.</li>
<li><b>Fri Night</b> - Got compiler working. Ported over CSS. Looks good, but doesn't work yet.</li>
<li><b>Sat</b> - Got 1 button working, then got all buttons working. Fixed all linting. Stuck on Refs (string => callbacks).</li>
<li><b>Sat Night</b> - Done</li>
</ul>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment