Do That Again: Loops

What Is A 'Loop'?

All the scripts up until now had a single directional flow from top to bottom. An exact order was listed and commands were executed one after the other, maybe skipping a few if you had an if command. However, what if you wanted to do those commands you just wrote again? Well you have a few options. The most straightforward approach would simply be writing those same commands again. While this may work if you only wanted it repeated twice, or if there is only one line you want repeated, this can get out of hand really quickly.

Luckily you don't have to do this, Denizen has 3 different loop commands that repeat a block of commands:

  • repeat to repeat a block a set number of times

  • foreach to repeat a block based on a list input, once for each entry in the list

  • while to repeat a block until a condition is met, like an if command that keeps going over and over until it stops passing

The Repeat Command

The most basic use of a loop is to simply run a block of commands again, which the repeat command is best suited for.

The repeat command, as its name implies, repeats a block of commands multiple times.

The basic format of a repeat command is:

- repeat (number of times):
    - (commands here)

This structure is similar to the if command, where you end the repeat command with a :. The commands that get looped need to also be indented in to let Denizen know that these are the commands you want to be looped.

Here is what it might look like in a real script:

my_lightning_task:
    type: task
    script:
    - repeat 5:
        - strike <player.location>
        - narrate "you have been struck by lightning!"
        - wait 1s
    - narrate "no more lightning"

Try this out in-game by running /ex run my_lightning_task.

This will loop 5 times, wherein the player will be struck by lightning as well as be told "you have been struck by lightning!" There is also a wait command at the end of the block. This pauses the script for a certain amount of time (in this example 1 second). This is important in many cases so that the commands don't run instantly after one another. In this example, without the wait it would seem as if the lightning strikes happened all at once.

Once the loop is done, the script continues, ending with "no more lightning."

What if you wanted to let the player know how many times they have been struck? You could add a definition that tracks the amount of loops, but repeat already does that for you. All you need to add is the as: argument to the repeat command like so:

my_lightning_task:
    type: task
    script:
    - repeat 5 as:count:
        - strike <player.location>
        - narrate "you have been struck by lightning <[count]> times!"
        - wait 1s
    - narrate "no more lightning"

The narrate will now say "you have been struck by lightning 1 times!", "you have been struck by lightning 2 times!", etc. up to 5.

The Foreach Command

While the repeat command is handy, you'll often find yourself wanting to loop over the contents of a list and do something with each entry in it.

Let's say we want to tell the player where all the cows around them are located. With the repeat command, you might do something like this:

my_cow_task:
    type: task
    script:
    - define cows <player.location.find_entities[cow].within[30]>
    - repeat <[cows].size> as:index:
        - define cow <[cows].get[<[index]>]>
        - narrate "There's a cow just <[cow].location.distance[<player.location>].round> blocks away!"
        - playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3

However, there is a much better way using the foreach command. The name "foreach" means literally "for each entry in the list, ..." and works exactly how this name implies - it re-runs the block of commands once for each entry in the list.

foreach has the same structure as repeat:

- foreach (some list) as:(definition to store each entry):
    - (commands here)

Here's how we can simplify that script with a foreach loop:

my_cow_task:
    type: task
    script:
    - foreach <player.location.find_entities[cow].within[30]> as:cow:
        - narrate "There's a cow <[cow].location.distance[<player.location>].round> blocks away!"
        - playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3

Try this in-game via /ex run my_cow_task while standing near some cows.

Much better. Most notably, we don't have have to get the entry cow from the list: it's already defined.

Additionally, foreach has a built-in definition of <[loop_index]> that keeps track of how many times it has looped. For an example of using that:

my_cow_task:
    type: task
    script:
    - foreach <player.location.find_entities[cow].within[30]> as:cow:
        - narrate "<[loop_index]> - There's a cow <[cow].location.distance[<player.location>].round> blocks away!"
        - playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3

The narrate will now be prefaced with a "1- ", "2- ", etc. as the loop goes on.

The While Command

You might encounter a situation where you don't know how many times you're going to loop, but you know when to stop. You can achieve this with a while loop. It's like an if command that runs the block below it as long as the condition is true.

- while (condition):
    - (commands here)

The condition format is the same as the if command; see that guide page for more details on its structure.

Now, lets see an example of a while loop...

my_move_task:
    type: task
    script:
    - define location <player.location>
    - while <player.is_online> && <[location].distance[<player.location>]> < 3:
        - narrate "You're too close, move away!"
        - wait 2s

You can try this in-game via /ex run my_move_task. This task, when run, will repeatedly tell you to move away until you have moved 3 blocks from where you first were.

Just like the repeat command, we have a wait command at the end of the loop. While it's only useful in some cases with repeat loops, you should almost always use it with while loops. Without one, it will try to check the condition constantly and repeatedly until it stops, which can cause the server to freeze until the while stops, or even crash if it never stops.

A word of warning: while loops should be avoided if possible. It is very easy to make what is known as an "infinite loop", as in a loop that never has a chance to stop. Once it's running, it's difficult to force it to stop beyond just stopping the server.

Additionally, some users write commands like while true to achieve scripts that intentionally run forever - this is not an ideal method, usually you should use the delta time event to achieve this.

Stop The Loop

Sometimes in loops you only want to keep looping until you reach a certain point. However, you may not want to stop the script entirely. While stop is used to stop scripts, repeat/foreach/while stop is used to stop the relevant loop.

For instance, in the first example, if we wanted to stop striking lightning if the player had less than 5 health, we would do as follows:

my_lightning_task:
    type: task
    script:
    - repeat 5 as:count:
        - strike <player.location>
        - narrate "you have been struck by lightning <[count]> times!"
        - if <player.health> < 5:
            - repeat stop
        - wait 1s
    - narrate "no more lightning"

Since we're using repeat stop, only the loop ends; the end narrate line still runs.

It can also be used in foreach loops. For instance, if we wanted to stop looping if we found a baby cow in the example above:

my_cow_task:
    type: task
    script:
    - foreach <player.location.find_entities[cow].within[30]> as:cow:
        - narrate "There's a cow <[cow].location.distance[<player.location>].round> blocks away!"
        - playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3
        - if <[cow].is_baby>:
            - narrate "omg its a baby!!"
            - foreach stop

Next Please

There are times when you want to skip to the next loop without finishing the current iteration. For this, you can use repeat/foreach/while next.

Take the foreach example we had, lets say we want to skip over all baby cows. While you can put the whole block inside an if command like so...

my_cow_task:
    type: task
    script:
    - foreach <player.location.find_entities[cow].within[30]> as:cow:
        - if !<[cow].is_baby>:
            - narrate "<[loop_index]> - There's a cow just <[cow].location.distance[<player.location>].round> blocks away!"
            - playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3

You could instead use next:

my_cow_task:
    type: task
    script:
    - foreach <player.location.find_entities[cow].within[30]> as:cow:
        - if <[cow].is_baby>:
            - foreach next
        - narrate "There's a cow <[cow].location.distance[<player.location>].round> blocks away!"
        - playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3