keep calling a function until a criteria is hit - javascript

writing a simple blackjack game
I want it to be semi automated.
I have this function after the player hits stick
stick = () => {
while(this.dealersTotal() < 21){
setInterval(this.dealToDealer, 3000)
}
}
however, when I do this it just freezes as I think im exceeding the stack. any ideas why this would do this? I can post more code but not sure seeing the other functions will help too much
dealersTotal is just a function that returns a number
dealToDealer just sets the state of dealers hand with the next card and then removes the top card from the deck

The function setInterval will be executed recurrently unless you stop it with the function clearInterval.
Use the setTimeout instead:
stick = () => {
while(this.dealersTotal() < 21){
setTimeout(this.dealToDealer, 3000);
}
}
Important: If this.dealersTotal() always returns a number less than 21 your code will enter in an infinite loop.

While loop can sick your perfomance, try to play around with using only interval.
Example:
stick = () => {
this.interval = setInterval(() => {
if(this.dealersTotal() < 21) {
this.dealersTotal();
clearInterval(this.interval)
}
}, 3000);
}

You have an infinite loop that is creating new intervals (which will run indefinitely each) on each iteration. That's what's killing your app.
The simplest solution would be (https://jsfiddle.net/29L556s9/10/):
stick = () => {
setTimeout(() => {
if(dealersTotal() < 21) {
dealToDealer();
stick();
}
}, 3000);
};

`Let interval = setInterval(()=>{
//Do what you want here
If(this.dealersTotal() >= 21){ clearInterval(interval) } // keep interval loop running until dealersTotal is bigger than or equal to 21
},50)//50ms`
With your current one, you are calling while loop and this loop will create a new setInterval so if you have 10 while loop, setInterval will be called 10 times which will create infinite interval which eventually exceeds processing power. So the best is to keep interval only as while loop will block. While loop is good for definite value or until something happens such as a keyboard press. But when you want a UI multiple things happening then setInterval is the better option.

Related

Is there a way to alternate setInterval durations?

I want to be able to call setInterval (or something similar) at two different lengths, alternating.
For example, running a function after 5 seconds, then 1 second, then 5 seconds again, and so on.
Is this possible? I tried a function that alternates the value, but it didn't seem to work.
let num = 5000
function alternateNum() {
if (num === 5000) { num = 1000 }
else { num = 5000 }
}
setInterval(() => {
// ...
alternateNum()
}, num);
JS timers have a very complicated history.
Using a recursive setTimeout invocation is a simple and elegant solution as long (as your runtime implements tail call optimization).
Separate from the issue of recursion is the issue of timer drift. This is covered in the YouTube video JavaScript counters the hard way - HTTP 203 if you'd like an accessible introduction.
In many JS engines (e.g. V8) setInterval will handle drift correction for you, so there's actually an advantage to using it over recursively invoking setTimeout. (Check the millisecond timestamps in the console messages in the snippet below to verify this.)
In order to determine the constant interval argument you'll need for setInterval, you'll need to find the greatest common factor of your delay durations. Once you have this value, you can use it as the base interval delay, and keep track of your interval state to determine whether you should switch to the next interval delay, run your other code, etc. Here's a minimal example:
const durations = [1000, 5000];
// If you can't determine this in advance and use a constant value,
// then you can calculate it at runtime using a function:
const gcf = 1000; // or const gcf = findGreatestCommonFactor(durations);
let durationIndex = 0;
let elapsed = 0;
function update () {
elapsed += gcf;
const ready = elapsed === durations[durationIndex];
if (ready) {
elapsed = 0;
durationIndex = (durationIndex + 1) % durations.length;
}
return ready;
}
setInterval(() => {
const ready = update();
if (!ready) return;
// Do your interval task, for example:
console.log('tick');
}, gcf);
The problem with setInterval() is that the time is taken into account just once. You can use setTimeout() with recursion instead:
function doAction(flipFlop) {
setTimeout(() => {
console.log(flipFlop ? 'flip' : 'flop');
doAction(!flipFlop);
// do some other action...
}, flipFlop ? 1000 : 3000);
}
doAction(true);
Watch out though if you have a long running process, this recursion gets deeper and deeper.
I think this method is the easiest:
setInterval(() => {
console.log("first");
setTimeout(() => console.log("second"), 750);
}, 2000);
This creates an interval that alternates between 1250 and 750 milliseconds.
The problem with your code
let num = 5000
function alternateNum() {
if (num === 5000) { num = 1000 }
else { num === 5000 }
}
setInterval(() => {
// ...
alternateNum()
}, num);
The last few lines (the setInterval) call are only getting called once with the initial value of num and thus any future changes to num won't be reflected in the setTimeout call.
How to fix it
You should use setTimeout within the function that has your code and call your function recursively:
const doStuff = (time = 1000) => {
// Your code here
// generate the next time to wait
const nextTime = time === 5000 ? 1000 : 5000;
// call the function again after waiting `time` milliseconds
setInterval(() => doStuff(nextTime), time);
}
Then you would call doStuff to start it. If you wanted to start it immediately with the next one happening after 1 second you could do:
doStuff();
Or if you wanted to call it after 5 seconds with the next one happening one second after that:
setTimeout(doStuff, 5000);
The difference here compared to your code is that the variable that represents the time is being used over and over again as it changes instead of just once on initial code execution.

How to stop a function when it's called?

I'm building a program that either counts down or up and I've got it working however I like to press count-up in the middle of count down or vice versa and I like the counter to stop and count up or vice versa. how do I achieve that? thanks a lot for your help :)
function myFunctionUp() {
var Timer = setInterval(function () {
i++;
document.getElementById("mydata").textContent = i;
if (i >= 21)
clearInterval(Timer);
if (i == 21){
document.getElementById("mydata").textContent = "Boom-up!";
}
}, 1000);
}
function myFunctionDown() {
var Timer = setInterval(function () {
i--;
document.getElementById("mydata").textContent = i;
if (i <= 0)
clearInterval(Timer);
if (i == 0){
document.getElementById("mydata").textContent = "Boom-down";
}
}, 1000);
}
Use a variable to keep track of the way to count. When a button is clicked, invert the value of the variable :
let countDown = 10;
let increment = -1;
function count() {
countDown += increment;
document.getElementById('container').innerText = countDown;
setTimeout(() => count(), 1000);
}
document.getElementById('btn').addEventListener('click', function () {
increment = -increment;
});
count();
Working stackblitz here
You typically never "take control" on the execution of another method. When you want to do that, the logic must be inverted. The function itself must ask if it should continue.
With an example : let's take a function which works in an infinite loop, that you want to be able to stop on demand. In some languages, you could run some code in a thread and interrupt the thread on demand. But even if it is possible, it is generally a bad idea to stop some code at the middle of its execution.
A better way of doing that is to create a "should Continue ?" piece of code at the end of the iteration. It could read a variable or call a method etc. When you want to stop the iteration, you just have to set this variable and you know that the infinite loop will stop graciously at the end of the current iteration

setTimeout being called multiple times

I can't figure out why setTimeout is being called multiple times in my code.
Here's a snippet of the code with what I thought was irrelevant removed:
let dead;
setup()
{
dead = false;
}
draw()
{
if(fell == true)
{
dead = true;
}
mechanics();
}
function mechanics()
{
let triggerVar;
if(dead == true)
{
triggerVar = 1;
dead = false;
}
if(triggerVar == 1)
{
setTimeout(resetG, 1500);
triggerVar = 0;
}
}
function resetG()
{
lives -= 1;
position = 0;
}
I can't tell what I'm doing wrong because whenever the character dies and setTimeout is called, it is actually not only called after the delay but also for the exact same duration after it is triggered. So in this case it is triggered first after 1500 millis and then every frame for another 1500 millis.
I managed to find the problem, which was not with the code I posted. The problem was that the constructor code that makes the object that changes dead to true if certain conditions are met was being called every frame from the moment it triggered death until the first instance of setTimeout kicked in, which means setTimeout was called every frame for 1500 milliseconds.
Chances are that you mechanics() function is called multiple times, you may give a variable to the settimeout like:
let timeoutID= setTimeout(resetG, 1500);
And in the proper place to clear it, for example after lifecycle
clearTimeout(timeoutID);

Counter in set Timeout nested in set interval is summed multiple times per callback

I have some code which is intended to update a counter that's inside of a set interval block. My intent is to have the counter updated once every time the set timeout function is called. But it seems that the callback for setTimeout is active for the entire duration of the timeout, so the counter is summed repeatedly over the 3 second timeout period, resulting in it having an end value of 8 rather than the desired value of 2.
I'm trying to understand how to implement this where the counter will be called immediately after the timeout, and only once. Unfortunately simply putting it after the function does not seem to solve this issue.
let count = 1;
let flag = 1;
setInterval(() => {
if (flag == 1) {
setTimeout(()=>{
flag = 0;
count++;
}
,3000);
}
},500);
that ?
let count = 1
, flag = 1
, noTimCall = true
;
setInterval(() =>
{
if (flag === 1 && noTimCall)
{
noTimCall = false
setTimeout(()=>
{
flag = 0
count++
noTimCall = true
}
,3000)
}
}
,500)
Let's go through your code step by step.
0ms since start. flag is 1 ⇒ 1st timeout created.
500ms since start. flag is 1 ⇒ …
…
3000ms since start. flag is 1 ⇒ 7th timeout created.
1st timeout is executed: Setting flag to 0, increasing count to 2.
3500ms since start. flag is 0 ⇒ Effectively nothing.
2nd timeout executed: …, increasing count to 3.
4000ms since start. flag is 0 ⇒ …
3rd timeout executed: …
…
6000ms since start. …
7th timeout executed: …, increasing count to 8.
As you can see, there will be 7 timeouts created, each one increasing count by one, ending with count at 8.
There are multiple ways to increase count only once after a delay:
Not using timeouts. We can keep track of how much time has passed manually, and increase count once a certain threshold (here: 3 seconds) is reached.
Changing the variables synchronously means they will already have their new values during the interval-callback, unlike using timeouts where they are changed after the callback.
Increase count if and only if flag is changed from 1 to 0 by that timeout-callback. Further:
Always create timeouts. The check inside them takes care of incrementing count only once, anyway.
Only create one timeout. This would require a new variable.
Create a timeout outside of the interval-callback. However, this means we cannot check when to start it from within our interval, as it is created outside immediately.
1. Keeping track of time
To keep track of time, we need to introduce a new variable that is increased by the interval's delay at the end of our interval-callback.
Now we only need to check the passed time against a threshold (here: 3000 milliseconds), and do a certain action once that condition is met.
We can further extend our code by allowing the if-block to be en-/disabled. This can be simply accomplished using a boolean.
let count = 1;
let flag = 1;
let timePassed = 0; // Tracks passed time
let isStoppingEnabled = true; // Allows toggling of the stopping if-block
// Re-starting the timer requires re-setting both variables above; see last arrow expression below
setInterval(() => {
if (flag == 1) {
// Runs once 'timePassed' reaches its threshold of 3000
// and this if-block is "enabled" (see 'isStoppingEnabled')
if (isStoppingEnabled && timePassed >= 3000) {
// Will "disable" _this_ if-block;
// disabling outer if-block using 'flag = 0;' also works
isStoppingEnabled = false;
count++;
}
}
console.log(count);
timePassed += 500; // Remember to increase 'timePassed' by the interval's delay
}, 500);
document.querySelector('#re-stop').addEventListener('click', () => {
isStoppingEnabled = true; // Re-enable said if-block
});
document.querySelector('#re-stop-track').addEventListener('click', () => {
isStoppingEnabled = true; // Re-enable said if-block and...
timePassed = 0; // re-start time tracking
});
body { /* Ignore; better styling */
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem 0.5rem;
}
<button id="re-stop">Re-enable stopping</button>
<label for="re-stop">
Since 'timePassed' will most likely be already<br>
over its threshold of 3000, 'count' will be increased almost immediately.
</label>
<button id="re-stop-track">Re-enable stopping and re-set time-tracker</button>
<label for="re-stop-track">
Will re-set 'timePassed', effectively re-starting<br>
the 3 second timer until 'count' is increased.
</label>
Note: Be wary of integer "overflows"! JavaScript won't warn about the usage of unsafe integers! Make sure to keep your time-tracking variable in the safe range of integers.
2. Using setTimeout()
We can create timeouts that will check whether they should execute or not using a simple if-statement.
2.1 Allow multiple timeouts
A simple one that will run when flag is set to 1 would look like this:
let count = 1;
let flag = 1;
setInterval(() => {
if (flag == 1) {
setTimeout(() => {
if (flag == 1) {
flag = 0;
count++;
}
}, 3000);
}
console.log(count);
}, 500);
However, that would create timeouts as long as the first timeout created hasn't run, since until then, the if-statement is executed, creating further timeouts. Technically, this will work just fine, but actually, it will create unnecessary timeouts producing unnecessary overhead.
2.2 Restricted timeout creation
We can restrict the amounts of timeouts at a time using a single boolean, since we only want to know if one was already created or not.
let count = 1;
let flag = 1;
// Prefixed with "_" (underscore) because it has
// no further relevance outside of this script; should actually be private
let _isTimeoutCreated = false;
setInterval(() => {
if (flag == 1) {
if (!_isTimeoutCreated) { // Only create a timeout when none was created before
_isTimeoutCreated = true; // Dis-allow creation of new timeout
setTimeout(() => {
_isTimeoutCreated = false; // Allow creation of new timeout
flag = 0;
count++;
}, 3000);
}
}
console.log(count);
}, 500);
3. Creating a timeout outside of setInterval()
The simplest way would be to create a single timeout after creating the interval.
Both will be created at almost the same time because timeouts and intervals are asynchronous, allowing further execution of the current call.
If they weren't, then the script would be stuck at that line, making your website unresponsive (because rendering and script-execution share the same thread).
Them being asynchronous (here meaning: "non-blocking") means, that while the interval executes, the timeout's timer will still tick down, executing after 3 seconds of your interval's creation.
The problem with this solution is, that it will only be a viable solution if you want to start the stopping process right away after starting your loop. You can start a new timeout from within your interval, but that would make you use either point 2.1 or point 2.2 again.
let count = 1;
let flag = 1;
setInterval(() => {
if (flag == 1) {
console.log(count);
}
}, 500);
setTimeout(() => {
flag = 0;
count++;
console.log(count);
}, 3000);
Endnote
Obviously, there are further ways to do this, like clearing the interval and creating a new one for starting/stopping it. That could be wrapped in a utility-class. Or one could use a Timer of an already existing library. Or one could find a way I couldn't think of at the top of my head.
An important last note would be to always think about the overhead you produce, especially in environments where performance matters. But(!) one shouldn't change his way to a different implementation only because of performance reasons, if they aren't necessary.

Why doesn't my setTimeout callback not get called?

I'm busy building a flash-card game. I want to give the user a visible countdown before a card get's flashed on screen. My script for the countdown looks like this:
let downSeconds = 5;
while (downSeconds > 0) {
setTimeout(function() {
$("#timerDisplay").text = downSeconds;
downSeconds--;
}, 1000);
}
$(".detail-card").removeClass("hidden");
If I didn't want the updated seconds I'd just use a 5000ms 'setTimeOut'. I did before try with a setInterval, with a delay of 1000ms, so every time it elapses it updates the seconds.
Now, if I put a breakpoint on either line of the setTimeOut callback, and only there, nothing happens when the setTimeout is invoked, so the seconds display never updates, and I'm in an infinite loop, because downSeconds--; is never invoked, so downSeconds keeps the value of 5 all throughout.
What am I doing wrong?
setTimeout runs the code later, while the while loop runs "now". You can't successfully combine the two.. So, you have to write your code differently, something like this should work:
let downSeconds = 5;
function doCountDown() {
downSeconds--;
$("#timerDisplay").text = downSeconds;
if (downSeconds > 0) {
setTimeout(doCountDown, 1000);
} else {
$(".detail-card").removeClass("hidden");
}
}
setTimeout(doCountDown, 1000);
You may use ES7 and await the loop for a second:
const time = ms => new Promise(res => setTimeout(res,ms));
(async function(){
let downSeconds = 5;
while (downSeconds > 0) {
await time(1000);
$("#timerDisplay").text = downSeconds;
downSeconds--;
}
$(".detail-card").removeClass("hidden");
})()
The setTimeout call is asynchronous,
and so by the time the 1000 milliseconds scheduled by the first call to setTimeout elapses,
the while-loop will have executed its body thousands of times,
each time scheduling a new job with setTimeout,
causing massive scheduling work to the JavaScript engine.
The engine is too busy to execute the function,
and it just keeps getting worse,
as the loop keeps running and keeps scheduling more and more.
I would expect your execution environment to become unresponsive and unable to make progress, unable to actually call the first function scheduled.
Use setInterval and clearInterval instead, for example:
let counter = 5;
let interval = setInterval(() => {
$("#timerDisplay").text = counter;
counter--;
if (counter == 0) {
clearInterval(interval);
$(".detail-card").removeClass("hidden");
}
}, 1000);

Categories