WarriorJSCode your way through dungeons, prove your skills, and get hired.WarriorJS The WarriorJS project brings a simple and fun interface into learning about JavaScript scripting and the basics of

a month ago

Latest Post Free Pluralsight Courses - April 2020 by Tyler Moon
WarriorJS
Code your way through dungeons, prove your skills, and get hired.

The WarriorJS project brings a simple and fun interface into learning about JavaScript scripting and the basics of artificial intelligence. By changing a set Player.js script, the WarriorJS system will run your script in set levels to battle enemies and climb towers in a 2d ASCII art way.

There are two ways of playing WarriorJS; online or via the NPM CLI. This article will cover using the NPM cli but the JavaScript is the same either way.

Prerequisites

Local Setup

First, create a new directory to hold the project with mkdir warriorjs-example. Then install the CLI via NPM with npm install -g @warriorjs/cli. To start up a new session run warriorjs and then follow the prompts. After it is started it will generate a directory like this:

warriorjs
 | the-first-steps
 | - .profile #tracks user progress
 | - Player.js # file that adds action to the player
 | - README.md # directions for the current level

To get the directions for what to do next read the README.md file inside the the-first-steps directory that was created. Every time that you solve a level the CLI will update the README.md file with the next instructions for that level. This article will not be duplicating the instructions but will be explaining some other concepts.

Note: This article will show an example of how to solve the tutorial levels. I would recommend that you try and solve the levels on your own before reading farther to get the whole experience.

Level 1 - The Basics

class Player {
  playTurn(warrior) {
    warrior.walk();
  }
}
Level 1 Example

Updating the Player.js file with the following, and then running warriorjs again will run the player through the first level. This is the most simple script for this game as it walks forward until the character hits the exit.

Level 2 - Learn to Fight

class Player {
  playTurn(warrior) {
    if(!warrior.feel().isEmpty()){
      warrior.attack();
    } else {
      warrior.walk();
    }
  }
}
Level 2 Example

In the second level, we encounter our first enemy that the character will have to fight. At this point, our character needs to be able to make a decision. A basic JavaScript if else statement is the simplest way to get started. While this technique can get out of hand (as we will see later in this article) for a basic level such as this its a good starting place.

Level 3 - Time to Heal

class Player {
  playTurn(warrior) {
    if(!warrior.feel().isEmpty()){                    // fight
      warrior.attack();
    }else if(warrior.health() < warrior.maxHealth()){ // heal
      warrior.rest();
    } else {                                     // default to walking
      warrior.walk();
    }
  }
}
Level 3 Example

The next ability our entrepot explorer gains is the ability to heal himself. In adding to the if else statement that was started in Level 2, we can add another if else in the middle with a condition for when to start healing with the rest() command. Order now matters and putting the rest() command above the attack() command may mean that an enemy will be attacking while you are healing, which is an issue.

Level 4 - Archers Attack

class Player {
  constructor(){
    this.health = 20;                          // Health class variable
  }

  playTurn(warrior) {
    var isTakingDamage = warrior.health() < this.health;

    if(!warrior.feel().isEmpty()){                    // fight
       warrior.attack();
    }else if(!isTakingDamage && 
            (warrior.health() < warrior.maxHealth())){ // heal
      warrior.rest();
    } else {                                   // default to walking
      warrior.walk();
    }
      
    this.health = warrior.health();
    }
}
Level 4 Example

Focus on movement if taking damage to rush archers. To determine if the character is taking damage we can set up a class variable this.health and update it at the end of every turn. If this.health is greater than warrior.health() (which is the current health of the character on that turn) then the character is taking damage. At this stage, the reaction to taking damage is to rush the archer and start attacking. Later on, this strategy will stop working so it will have to be updated but in this early level its enough.

Level 5 - Free the Captives!

class Player {
    constructor(){
        this.health = 20;
    }

    playTurn(warrior) {
        var isTakingDamage = warrior.health() < this.health;
        if(warrior.feel().getUnit() !== undefined 
            && warrior.feel().getUnit().isBound()){ // free captive
            warrior.rescue();
        } else if(!warrior.feel().isEmpty()){ // fight
            warrior.attack();
        } else if(!isTakingDamage 
            && (warrior.health() < warrior.maxHealth())){ // heal
            warrior.rest();
        } else { // default to walking
            warrior.walk();
        }

        this.health = warrior.health();
    }
}
Level 5 Example

At this level the if else statement strategy is starting to get out of control. Now we have to not only check for enemies to attack, but also make sure that the character does not attack any friendly captives but instead frees them. The rescue() command has to come before the attack() command then so in this example it is the first check-in the logic check.

Level 6 - To the Back

Now direction matters which throws a wrench in our current implementation. Most of the actions can take in an option string parameter of forward or backward to denote a direction. While we could type out this string all over the code this is a good opportunity to introduce a constant object for these static values. Using constant objects is useful so that you, or other developers if on a team, do not have to remember the capitalization or exact terms.

Introduce constant objects for static values

const Directions = {
    forward: 'forward',
    backward: 'backward'
};

class Player {
    ...
Directions Constant Object

At this level, the enemies are too spread out to rush the archers when taking damage. To resolve this we can move the istakingdamage up.

// Direction Constants
const Directions = {
  forward: 'forward',
  backward: 'backward'
};

class Player {
  constructor(){
    this.health = 20;
    this.currentDirection = Directions.backward;
  }

  playTurn(warrior) {
    // Current state
    var isTakingDamage = warrior.health() < this.health;
    var isFacingWall = warrior.feel(this.currentDirection).isWall();

    // if facing wall reverse
    if(isFacingWall){
      this.currentDirection = this.currentDirection == Directions.forward ? Directions.backward : Directions.forward;
    }

    // Main Actions
    if(isTakingDamage && 
      (warrior.health() < (warrior.maxHealth() * 0.5))) { // run away
      warrior.walk(Directions.backward);
    } else if(warrior.feel(this.currentDirection).getUnit() !== undefined 
            && warrior.feel(this.currentDirection).getUnit().isBound()){ // free captive
      warrior.rescue(this.currentDirection);
    } else if(!warrior.feel(this.currentDirection).isEmpty()){ // fight
      warrior.attack(this.currentDirection);
    } else if(!isTakingDamage && warrior.health() < warrior.maxHealth()){ // heal
        warrior.rest();
    } else { // default to walking
      warrior.walk(this.currentDirection);
    }

      this.health = warrior.health();
    }
}
Level 6 Example

Level 7 - Pivot

// Direction Constants
const Directions = {
    forward: 'forward',
    backward: 'backward'
};

class Player {
    constructor(){
        this.health = 20;
        this.currentDirection = Directions.forward;
    }

    playTurn(warrior) {
        // Current state
        var isTakingDamage = warrior.health() < this.health;
        var isFacingWall = warrior.feel(this.currentDirection).isWall();

        // Main Actions
        if(isFacingWall){ // if facing wall reverse
            warrior.pivot();
        } else if(isTakingDamage && (warrior.health() < (warrior.maxHealth() * 0.5))) { // run away
            warrior.walk(Directions.backward);
        } else if(warrior.feel(this.currentDirection).getUnit() !== undefined 
            && warrior.feel(this.currentDirection).getUnit().isBound()){ // free captive
            warrior.rescue(this.currentDirection);
        } else if(!warrior.feel(this.currentDirection).isEmpty()){ // fight
            warrior.attack(this.currentDirection);
        } else if(!isTakingDamage && warrior.health() < warrior.maxHealth()){ // heal
            warrior.rest();
        } else { // default to walking
            warrior.walk(this.currentDirection);
        }

        this.health = warrior.health();
    }
}
Level 7 Example

While the character can indeed attack in the backward direction, there is a penalty in doing so. One of the actions feel() can tell if a character is facing a wall. So at this point, we can use the pivot() action if the character is facing a wall to turn around making backward forward and vice versa.

Level 8 - Bow and Arrows

// Direction Constants
const Directions = {
  forward: 'forward',
  backward: 'backward'
};

class Player {
  constructor(){
    this.health = 20;
    this.currentDirection = Directions.backward;
  }

  playTurn(warrior) {
    // Current state
    var isTakingDamage = warrior.health() < this.health;
    var isFacingWall = warrior.feel(this.currentDirection).isWall();
    var isCaptiveInRange = this.isEnemyInSight(warrior.look(this.currentDirection));
        
    // Main Actions
    if(isFacingWall){ // if facing wall reverse
      warrior.pivot();
    } else if(isTakingDamage && (warrior.health() < (warrior.maxHealth() * 0.5))) { // run away
            warrior.walk(Directions.backward);
    } else if(warrior.feel(this.currentDirection).getUnit() !== undefined 
     && warrior.feel(this.currentDirection).getUnit().isBound()){ // free captive
      warrior.rescue(this.currentDirection);
    } else if(isCaptiveInRange) {
      warrior.shoot(this.currentDirection);  
    } else if(!warrior.feel(this.currentDirection).isEmpty()){ // fight
      warrior.attack(this.currentDirection);
    }  else if(!isTakingDamage && warrior.health() < warrior.maxHealth()){ // heal
      warrior.rest();
    } else { // default to walking
      warrior.walk(this.currentDirection);
    }

      this.health = warrior.health();
    }

    isEnemyInSight(lookArray) {
      const spaceWithUnit = lookArray.find(space => space.isUnit());
      return spaceWithUnit && spaceWithUnit.getUnit().isEnemy();
    }
}
Level 8 Example

Seeing as how WarriorJS is using plain JavaScript, you can write custom methods outside of the playTurn(warrior) method that is provided. In level 8 we can add an isEnemyInSight() method to check if an enemy is anywhere within a 3 space array that the main character can see using the look() action.

Because the look() action returns a plain JavaScript array we can use all the built-in JS array methods. In this case, we can use the find(space => space.isUnit()) method to search through the array for an object that is a unit.

Check out this article for a rundown of available array methods: https://www.geeksforgeeks.org/javascript-basic-array-methods/

Level 9 - Put it Together

The previous level example works for Level 9 but is not ideal as it leaves one of the two captives on the map. This means that while the level is beaten the character does not achieve the highest score possible. In a future article, we will cover "AI" methods for improving level 9.

Summary

The WarriorJS project is a great way to learn, or brush up on, JavaScript programming skills. Going forward it is also a great platform for learning the basics of AI game programming. In a future article, we will cover more of these methods building off this super basic script developed in this article.

Tyler Moon

Published a month ago