Reputation: 41
I am still a JS beginner, trying to get to grips with jQuery and started coding up the Simon Game as a project. You have 4 colored spaces that should light up & play a sound in a random sequence, starting with a sequence of length 1. The player recreates that sequence by clicking on the squares. If successful the random sequence length will be incremented by 1, if you make a mistake you have to start over.
I am at the stage of simply creating that random sequence of length 5 and visually showing the sequence. That's what I've come up with:
var sequence = [];
$("button").click(startGame);
function startGame() {
for (let index = 0; index < 5; index++) {
sequence.push(nextColour());
touchField(index);
// playSound();
}
}
function nextColour() {
var randomNumber = Math.floor(Math.random() * 4) + 1;
if (randomNumber === 1) {
var nextColour = "red";
} else if (randomNumber === 2) {
var nextColour = "blue";
} else if (randomNumber === 3) {
var nextColour = "green";
} else {
var nextColour = "yellow";
}
return nextColour
};
function touchField(index) {
$("." + sequence[index]).addClass("active");
setTimeout(function() {
$("." + sequence[index]).removeClass("active")
}, 300);
}
.active {
border: 1rem solid purple;
}
.red {
border: 1px solid red;
}
.blue {
border: 1px solid blue;
}
.green {
border: 1px solid green;
}
.yellow {
border: 1px solid yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<img class="red" src="https://via.placeholder.com/50" alt="">
<img class="blue" src="https://via.placeholder.com/50" alt="">
<img class="green" src="https://via.placeholder.com/50" alt="">
<img class="yellow" src="https://via.placeholder.com/50" alt="">
So the issue is, that the class ".active" gets added and removed from all the squares almost simultaneously - which makes it impossible to distinguish any kind of sequence. So I thought I can use "setTimeout();". That does not prevent it from doing it though. I believe what happens is, that until the expression which is to be evaluated after the timeout is indeed executed, the code simply keeps on running, thus adding the ".active" and the time out 5 times almost simultaneously - which leads to their almost simultaneous removal. Ideally there is sufficient time between removing ".active" and adding ".active" to the next square. I also tried adding setTimeout to the "touchSquare()" function call. It doesn't work either - I believe for the same reason, it just keeps executing. I hope you see my problem. ^^
I have looked at other questions, but the answers seem to disregard the fact that the browser keeps executing code after it got to the setTimeout, like here for example:
How to delay execution in between the following in my javascript
If you copy paste that into the console, it does not at all do what it is supposed to exactly because the code keeps being executed after the setTimeout is recognised. I hope I could make my problem make sense to you, it's the first time I am posting a question here on StackOverflow. If I can improve the way I ask a question in any way I am very grateful for constructive criticism!
Upvotes: 1
Views: 772
Reputation: 1073
To understand the reason it appears to be ignoring the setTimeout()
and things appear to happen simultaneously, you need to understand the difference between the stack
and the event loop
.
Concurrency model and the event loop
Things put onto the stack happen immediately (or as soon as possible), which is what the for
loop is doing.
Things put into the event loop happen after a given minimum amount of time (AFTER, not exactly on), which is what the setTimeout
is doing.
In other words, the for loop is putting the setTimouts into the event loop within milliseconds of each other, and therefore the timeouts are firing within milliseconds of each other. The event loop doesn't work like a do this, then this, then that. It works like a "has this event met or exceeded it's timeout? If yes, execute it as soon as possible"
To achieve the effect you are looking for, you would need some recursion, or when a function calls itself. That way the next TouchField()
isn't called until the previous one has completed, spacing things out the way you are expecting them to be.
I've done some non beginner things to simplify generating a random sequence (using Array.from() and an array of values rather than a lengthy if/else block), and I've added a second timeout so that there is a bit of delay between showing fields, (otherwise the same field twice in a row blurs together), but hopefully this helps you understand the concept.
let level = 5, sequence; //increment the level /sequence length as the player progresses
$('button').click(function startGame() {
sequence = Array.from({ length: level },
field => ['red','blue','green','yellow'][Math.floor(Math.random() * 4)]);
TouchField(); //no argument to start recursion / show sequence
});
function TouchField(index = 0) {
if (index <= sequence.length - 1) { //recursion exit condition
// playSound();
$('.' + sequence[index]).addClass('active');
setTimeout(function() {
$('img').removeClass('active');
setTimeout(function() {
TouchField(index + 1); //recursion
}, 500);
}, 500);
}
}
.active {
border: 1rem solid purple !important;
}
.red {
border: 1px solid red;
}
.blue {
border: 1px solid blue;
}
.green {
border: 1px solid green;
}
.yellow {
border: 1px solid yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<img class="red" src="https://via.placeholder.com/50" alt="">
<img class="blue" src="https://via.placeholder.com/50" alt="">
<img class="green" src="https://via.placeholder.com/50" alt="">
<img class="yellow" src="https://via.placeholder.com/50" alt="">
<div>
<button>PLAY</button>
</div>
Upvotes: 1
Reputation: 350137
One way is to use setTimeout
as an asynchronous loop, i.e. where the callback will schedule a new timer with a repeated setTimeout
call. But as you will want to continue with other stuff after that finishes, you'll soon get into what is called "callback hell".
I would suggest that you incorporate promises. JavaScript has async
and await
syntax to ease the programming with promises.
So the first thing to do is create the equivalent of setTimeout
, but as a function that returns a promise, which you can await
.
Then the rest of the code needs little change.
I would however suggest to :
sequence
array, instead of the color names.sequence
array at every call of startGame
.Here is how it could work. Ignore the additional CSS which I added, as I didn't have access to your images.
const sequence = [];
// Load the elements in an node list
const $images = $(".red, .blue, .green, .yellow");
$("button").click(startGame);
// Helper function
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function startGame() {
$("button").hide(); // Don't allow a click on Start while the sequence is animating
sequence.length = 0; // reset
for (let index = 0; index < 5; index++) {
sequence.push(nextColour());
await touchField(sequence[index]);
}
$("button").show();
console.log(sequence);
// Continue with other logic here...
}
function nextColour() {
return Math.floor(Math.random() * 4);
};
async function touchField(index) {
$images.eq(index).addClass("active");
await delay(300);
$images.eq(index).removeClass("active");
await delay(100);
}
.active {
border: 1rem solid purple;
}
.red { background-color: red; }
.blue { background-color: blue; }
.green { background-color: green; }
.yellow { background-color: yellow; }
img {
display: inline-block;
width: 70px;
height: 50px;
border: 1rem solid white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<img class="red" src="/projects/simon-game/images/red.png" alt="">
<img class="blue" src="/projects/simon-game/images/blue.png" alt="">
<img class="green" src="/projects/simon-game/images/green.png" alt="">
<img class="yellow" src="/projects/simon-game/images/yellow.png" alt="">
<p>
<button>Start</button>
Upvotes: 1