DEV Community

Cover image for Learn serverless programming playing a game
Michele Sciabarra
Michele Sciabarra

Posted on

Learn serverless programming playing a game

FAAS Wars is a free programming game where you have to write the code that controls a "FAAS fighter". It is availablehere.

The fighter is a spaceship that can move in space and shoot lasers. The goal of the game is to defeat the enemy fighter, hitting it 5 times, and of course avoiding the bullets of the enemy.

To control a fighter you have to write the control code. The control code itself is a Nimbella serverless action. In the following, there is a tutorial about how to write a progressively smarter control action.

We are using javascript as the programming language. You can use however any other programming language available in Nimbella, like Python or Go. All the actions receive their input in JSON format and return the output in JSON, too. So the logic described in javascript can be readily translated into any other programming languages.

Let's start now, discussing how to create your fighter control code with a step by step tutorial.

How to control your fighter

A serverless action suitable for Faas WARS in its simplest form has this format:

function main(args) {
    return {"body": []}
}
Enter fullscreen mode Exit fullscreen mode

Any Nimbella action returns a map. In our case, we need a web action returning JSON, so you have to use body as a mandatory field of your object. The resulting answer has to be an array of maps. In the simplest case, it is just an empty array. However, if you implement this action way your fighter will simply do nothing at all. Just sit down waiting to be hit by the enemy.

You can send commands to the fighter. A command is a map, where the keys are the commands given to the robot and the values are the parameters of the command. For example, you can order the fighter to "yell" something. If you want your robot displays the current time, you can order it with { "yell": new Date().toLocaleTimeString()}. Let's put it in an entire action:

function main(args) {
    let msg = new Date().toLocaleTimeString()
    return {"body": [{"yell":msg}]}
}
Enter fullscreen mode Exit fullscreen mode

If you start the battle, you can see not the fighter is telling the current time. As it is not doing anything else, it will not survive for very long if there is a minimally offensive another fighter in the battleground. Indeed, this example is not very useful, battle-wise, but nonetheless, we see our robot is doing something, now!

Let's learn how to move around our fighter. As we already pointed out, the action returns an array of commands, so you can give multiple orders.

Out first step is to order to the robot to move forward and then turn to the left, as follows:

function main(args) {
    return {"body": [
       {"move_forwards":50},
       {"turn_left":90},
    ]}
}
Enter fullscreen mode Exit fullscreen mode

If you run this code you will note the fighter will move around, following a squared path. Indeed the orders are to move forward 100 pixels and then turn right of 90 degrees, forever. If it is hit, it may change randomly its orientation.

Reacting to events

If you run the robot this way, is it blind and stupid, but it has not to be this way. Actually, the robot receives information about its environment in the args parameter. The most important value to check is args.event. There are basically 4 events our robot can react to:

  • idle: when the robot is running out of commands and has nothing to do
  • enemy-spot: when the robot sees the enemy right in front of the turret.
  • hit: when an enemy bullet hits the robot
  • wall-collide: when the robot hits the wall and cannot move forward anymore

Now lets add the ability to shot to the enemy when it sees one. For this purpose we instroduce a swich on the event. Also, we use an array where we push the actions we want to sent. Our revised code is thus:

function main(args) {
    let actions = []
    switch(args.event) {
        case "idle":
            actions.push({"move_forwards":50})
            actions.push({"turn_left":90})
            break;
        case "enemy-spot":
            actions.push({"yell": "Fire!"})
            actions.push({"shoot":true})
            break;
    }
    return {"body": actions}
}
Enter fullscreen mode Exit fullscreen mode

Now, another detail. We say command is wrapped in a map, but in a map, there has not to be only one command. It can be multiple commands at the same time. But those have to be something the robot can do at the same time.

So for example a robot cannot move at the same time forward and backward, or move and turn. So the following actions are "sequential", in the sense, you can put only one at a time in the command maps:

  • move_forwards <number>: move forward of the given number of pixels
  • move_backwards <number>: move backward of the given number of pixels
  • move_opposide <number>: move in the opposite direction, useful when you hit a wall
  • turn_left <degrees>: turn the robot to the left of the given degrees
  • turn_right <degrees>: turn the robot right of the given degrees

However, you can order at the same time to yell and shot, for example. So this is a valid command: {"yell": "Fire!", "shoot":true}. Those are parallel actions.

In addition, you can also move the turret. This is thus the complete list of parallel actions:

  • yell <message> show a message
  • shot: true: order to shot, if the value is true
  • turn_turret_left <degrees>: turn the robot to the left of the given degrees
  • turn_turret_right <degrees>: turn the robot to the right of the given degrees
  • data: <object>: store the object and return it in every further event

Now let's put it all together, handling also the cases of the robot colliding with the wall or being hit. What it follows is the default control program that is the default when you create a new robot.

function main(args){
    let actions = []
    switch(args.event) {
        case "idle":
            actions.push({"turn_turret_left": 45, "move_forwards": 50})
            actions.push({"turn_left": 45})
            break;
        case "wall-collide":
            actions.push({"move_opposide":10})
            actions.push({"turn_left":90})
            break;
        case "hit":
            actions.push({"yell": "Ooops!"})
            break
        case "enemy-spot":
            actions.push({"yell": "Fire!", "shoot":true})
            break
        default:
            console.log(args)
    }
    return { "body": actions}
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)