Robo-Reindeer RUMBLE! — a festive MiniScript programming game

I’m happy to present the first public release of Robo-Reindeer Rumble, a fun little programming game for the holidays.

This game is based on MiniScript, a new scripting language I’m working on designed to work especially well with Unity. MiniScript features a clean syntax with minimal punctuation; a small yet complete set of types including strings, lists, and maps; and a decent library of intrinsic functions.

It is also very easy to extend and embed in a Unity app, which is what led to Robo-Reindeer Rumble!

This is the very first functional release of the game, so options for the deer are pretty limited. They can damage other deer in two ways: ramming into them at least 50% of full speed, or by throwing snowballs with the “throw” function.

For example, here’s a script for a reindeer that turns until it sees an enemy, then charges, full speed ahead!

// Name: Charger
while 1
    wait(0.01)
    see = look
    if see != null then
        // see an enemy; don't turn, but run straight ahead
        print("CHARGE!!!")
        speed = 100
    else
        // don't see anything, so let's slow down and turn a bit!
        print
        heading = actual.heading + 5
        speed = 20
    end if
end while

And here’s one that just turns in place until it sees an enemy, and then throws snowballs:

// Name: Spinner
print("I spin!")
while 1
    wait(0.01)
    see = look
    if see != null then
        print("Throw at E" + see.energy + " at " + see.distance + "!")
        throw
        heading = heading + rnd * 10 - 5
        wait(0.5)
    else
        // don't see anything, so let's turn a bit!
        heading = actual.heading + 5
    end if
end while

You can copy this code, switch to the game, click the Edit button by any reindeer, and paste the code right in. Use it as a starting point for your own designs, or start from scratch!

Let’s use this thread as a place to share our reindeer scripts — and when it’s not just me any more, I’ll start an almost-daily “king of the hill” style tournament. Can you build a smarter robot reindeer?

http://luminaryapps.com/temp/RoboReindeer/

3 Likes

I’ve updated the game with sound effects, a new optional parameter for throw, and a new deerCount variable that returns the number of robo-reindeer still in the arena.

The parameter for throw specifies how much energy to put into the snowball. This determines how much damage is done to the target if it hits, but it is also subtracted from your own energy store. If your energy goes negative, you are completely frozen until your energy recovers past zero.

For example, here’s a deer that spins in a circle until it sees a deer, and then attempts to one-hit kill it by putting 100 energy into a single throw:

// Name: One-Hit Hilda
while 1
    wait(0.01)
    see = look
    if see != null then
        print("Take THAT!")
        throw(100)
    else
        // don't see anything, so let's turn a bit!
        heading = actual.heading + 5
    end if
end while

If it doesn’t miss, this is devastating, as no reindeer can have more than 100 hit points. But it also leaves itself very vulnerable to attack while it recharges. (FYI, robo-reindeer are solar powered.)

Why not try it yourself? Just copy the above, run the game, Edit any reindeer, and paste it in.

I can think of two obvious ways to improve this: use the health property of the look result to only put as much energy in the snowball as it should take to destroy the target. (But if the snowball misses the target and is lucky enough to hit someone else, then it may not be enough for that one.) Or, use the new deerCount variable to change your behavior based on how many other deer are around.

Can anyone make a reindeer script that reliably beats the one above?

It’s been suggested to me that I need to have some better built-in starter scripts than the “Hello World!” scripts you get now. So, that’s on my to-do list… but in the meanwhile, here’s another example.

// Name: Sprayer
// This reindeer simply spins around rapidly, spewing low-energy
// snowballs in all directions.  Hopefully enough of these will
// hit the other reindeer to wear them down!
print("Whee!!!")
while 1
    heading = actual.heading + 100
    if energy > 5 and rnd < 0.1 then
        throw(5)
    end if
    wait(0.1)
end while

Also, note that I added an actual map, containing heading and speed. Since the reindeer has a limited acceleration & turning capability, the heading & speed you assign won’t be reached right away. So now you can look at actual.heading and actual.speed to find out where what your deer really is doing at any instant.

Here is more information on the extensions to MiniScript which are specific to Robo Reindeer code.

heading
This is a global variable which to which you can assign the desired heading (direction) for your reindeer. It is in degrees from 0 to 360; 0 is due East, 45 is Northeast, and so on. (You can assign values outside that range, but they will be remapped to the standard range on the next frame.) Robo-reindeer can’t turn instantly, and can’t turn at all when they’re out of energy, but they will turn (whichever way is shortest) to this heading as soon as they can.

speed
This global variable sets your reindeer’s target speed, as a percentage of maximum speed. Negative values shift your robo-reindeer into reverse. The valid range for this is -50 to 100. Just as with heading, the reindeer may will achieve this speed as quickly as possible, given a limited acceleration rate and energy. Note that movement consumes energy in proportion to your actual speed, and when the reindeer’s energy level is below 5, it will start to slow down (until at an energy level of 0 or less, it can’t move at all).

(heading and speed are the only special variables you should assign to, though of course you can make up your own variables and assign to those as you please.)

actual
This is a global variable you should only read, not set. It returns a map containing two elements: heading and speed. These are the actual current heading and speed of your robo-reindeer (as opposed to the global variables above, which are your target heading and speed). So, actual.heading is which way your reindeer is facing, and actual.speed is how fast your reindeer is moving.

deerCount
You can tell how many robo-reindeer are still in the arena by this global variable. This count includes yourself.

energy
This global variable tells you your robo-reindeer’s energy level. Energy is used for almost everything a robo-reindeer does: movement, turning, throwing snowballs, even thinking. Energy slowly increases (robo-reindeer are solar powered) up to a maximum of 100. Energy may go negative (dipping into emergency reserve power), but the reindeer will be frozen in place until its energy level increases past zero.

health
Use this global variable to read your reindeer’s current hit points. These start at 100, and when they reach zero, the robo-reindeer is scrapped.

position
This global variable is a map containing the current x and y position of your reindeer. position.x values go from -50 (left side of the arena) to +50 (right). position.y ranges from -50 (bottom of the arena) to +50 (top).

look
This is a function that returns information on the closest reindeer within 5° of your current (actual) heading. If there is no reindeer in that direction, it will return null. Otherwise, the result is a map containing:

  • distance: distance to the reindeer seen (in the same units as position)
  • heading: which way the other reindeer is facing
  • speed: how fast the other reindeer is moving
  • energy: how much energy the other reindeer has
  • health: how many hit points the other reindeer has left

throw(energy=20)
This function is used to throw a snowball. You can choose how much energy to put into the snowball; this amount is subtracted from your own energy, and is how many points of health are subtracted from any reindeer hit. If you say throw with no parameters, it throws a snowball with 20 energy. The snowball begins just in front of your reindeer, and travels in the direction of your current (actual) heading.

These are the only extensions to the standard MiniScript language. You now know everything there is to know about programming robo reindeer… what can you make them do?

@JoeStrout - The parser doesn’t seem to handle Windows’ CR-LF line terminators. Any way you could make it so I can paste from Windows?

Doh! I thought I was handling any standard line ending. To be clear, you can paste into the TextField and it looks correct there, but when you check syntax, you get an error?

And, do you get the same problem when you just type code into the TextField directly, hitting Return after each line? A simple test would be:

print(1)
wait
print(2)

EDIT: OK, I’ve fired up my Windows box to check it out. It appears that the code works fine when you type it in, but when I paste it in from Notepad, I get a “got Unknown where EOL expected” error.

(I also find that when I copy a code block from this web page, it not only includes the code, but the line numbers and two extra line breaks per line… at least when using IE. And that has nothing to do with Unity, because I get exactly the same thing pasting the code into Notepad. Seriously, how do people ever get anything done on Windows?)

I’ll dig into that line break problem right now!

OK, I’ve fixed the problem, and @TonyLi , you were right: it was a failure to account for Windows line endings in the lexer.

Though I still find it odd that the InputField doesn’t normalize line endings in any way (you apparently can get different results typing them vs. pasting them on Windows), but MiniScript (and thus RoboReindeer) now deals with any combination of line endings you may throw at it.

I look forward to seeing what you come up with!

Thanks, Joe! I’ll make a little time tomorrow to get something written and posted!

1 Like

I’ve updated the game with a big new feature: Meadow Mines! These are little piles of pure destruction. Every point of energy you put into them does two points of damage to any reindeer that steps on it! Simple call drop(energy, dely) to drop a meadow mine with the given energy and arming delay.

Here you can see the green reindeer has laid a couple of mines. The one on the right is still fresh (not yet armed). The one on the left is armed and dangerous.

…And, here’s the same reindeer a moment later, running over his own (armed) meadow mine.

So yes, you can step on your own mines; once they’re out they don’t care where they came from.

Incidentally, I’ve also updated the default script that reindeer start with for a new player. The starter script is now this:

print("Edit me!")
while 1
    heading = rnd * 360
    speed = 50 + rnd * 50
    wait(rnd * 2)
    speed = 0
    wait(rnd)
    if rnd < 0.1 then
        throw(20)  // throw a snowball with 20 energy
    else if rnd < 0.2 then
        drop(10, 2) // drop mine with 10 energy and 2-second delay
    end if
end while

This makes a reindeer that wanders around a bit aimlessly, occasionally dropping mines or throwing snowballs (again, aimlessly). It’s a much more interesting start than the previous Hello World script, and demonstrates some of the features you will probably want to include in your own robo-reindeer brains. (Note that if you’ve played before, you will probably still have the old Hello World scripts stored in your player prefs. Just copy/paste the above, or flush browser data.)

So now there are three ways to attack your opponents: throwing snowballs, dropping mines, and good old-fashioned bashing into them. Why not give it a try?

Okay, here’s a challenger:

// Blitzen:

min = function(a,b)
    if (a < b) then
       return a
    else
       return b
    end if
end function

ticksLeft = 0
headingDelta = 5
wanderSpeed = 30
while 1
    wait(0.01)
    see = look
    if (see != null) then
        speed = 100
        print("Moo!")
        if ((see.distance < 5) and (energy > 30)) then
            throwEnergy = min((energy - 20), see.health)
            print("Blitz!")
           throw(throwEnergy)
        end if
    else
        ticksLeft = ticksLeft - 1
        if (ticksLeft <= 0) then
            ticksLeft = 100 * rnd
           if (rnd < 0.5) then
                headingDelta = -5
            else
                headingDelta = 5
            end if
            wanderSpeed = 30
        end if
        heading = actual.heading + headingDelta
        if (wanderSpeed > 20) then
            wanderSpeed = wanderSpeed - 0.5
        end if
        speed = wanderSpeed
    end if
end while

He seems to do more harm than good with meadow mines, so I left them out.

Is there a way to detect mines?

1 Like

@TonyLi , that’s great! Your Blitzen seems like a strong contender indeed!

No, there is no way to detect mines (or snowballs, for that matter). Safe use of them would rely on moving in a way that ensures you don’t trip over your own mines. So, yeah, the way Blitzen dashes around the field, he’s better off without them.

One note on MiniScript style: parentheses aren’t needed around the condition in an “if” (or “while”) statement, and operator precedence follows the standard convention. So “if ((see.distance < 5) and (energy > 30)) then” could be written as just “if see.distance < 5 and energy > 30 then”. Getting rid of unnecessary punctuation was one of my design goals for the language… though of course, I can’t stop you from throwing in extra parens if you insist! :slight_smile:

P.S. Also, your ticksLeft works fine for counting loop iterations if you like, but also note the time intrinsic function, which returns the elapsed time since the start of the script. It might be handy in cases like this.

@JoeStrout - Well, then you really won’t like this code. :wink: I wanted to do a quick FSM model, but I didn’t take the time to design code for re-use so there’s a lot of duplication. But since that’s all the time I have for today, so be it.

Also, this raises an error:

if see != null and (see.distance < 10 or didFullSweep == 1) then

Presumably because MiniScript doesn’t do short-circuit evaluation?

// Corner lurker:
min = function(a,b)
    if (a < b) then
       return a
    else
       return b
    end if
end function

GotoLowerLeft = function()
    print("Going to lower left")
    globals.heading = 225
    globals.speed = 100
    position = globals.position
    while ((position.x > -49) or (position.y > -49))
        wait(0.1)
        if ((globals.energy > 30) and (position.x > -40)) then
            drop(2, 1)
        end if
    end while
    return @ScanInLowerLeft
end function

GotoUpperLeft = function()
    print("Going to upper left")
    globals.heading = 135
    globals.speed = 100
    position = globals.position
    while ((position.x > -49) or (position.y < 49))
        wait(0.1)
        if ((globals.energy > 30) and (position.x > -40)) then
            drop(2, 1)
        end if
    end while
    return @ScanInUpperLeft
end function

ScanInLowerLeft = function()
    print("Scanning from lower left")
    globals.speed = 0
    actual = globals.actual
    headingDelta = 5
    didFullSweep = 0
    myHealth = globals.health
    while 1
        if (globals.health < myHealth) then
            return @GotoUpperLeft
        end if
        wait(0.01)
        see = look
        hasTarget = (see != null)
        if hasTarget then
            isClose = (see.distance < 10)
            hasTarget = isClose or didFullSweep
        end if
        if (hasTarget) then
            throwEnergy = see.health
            if (didFullSweep) then
                throwEnergy = 5
            end if
            throw(throwEnergy)
            didFullSweep = 0
        else
            globals.heading = actual.heading + headingDelta
            if (actual.heading <= 0) then
                globals.heading = 5
                headingDelta = 5  
                didFullSweep = 1
            else if (actual.heading > 90) then
                globals.heading = 85
                headingDelta = -5
            end if
        end if
    end while
end function

ScanInUpperLeft = function()
    print("Scanning from upper left")
    globals.speed = 0
    actual = globals.actual
    headingDelta = 5
    didFullSweep = 0
    myHealth = globals.health
    while 1
        if (globals.health < myHealth) then
            return @GotoLowerLeft
        end if
        wait(0.01)
        see = look
        hasTarget = (see != null)
        if hasTarget then
            isClose = (see.distance < 10)
            hasTarget = isClose or didFullSweep
        end if
        if (hasTarget) then
            throwEnergy = see.health
            if (didFullSweep) then
                throwEnergy = 5
            end if
            throw(throwEnergy)
            didFullSweep = 0
        else
            globals.heading = actual.heading + headingDelta
            if ((actual.heading == 0) or (actual.heading == 360)) then
                globals.heading = 355
                headingDelta = -5  
                didFullSweep = 1
            else if (actual.heading < 270) then
                globals.heading = 275
                headingDelta = 5
            end if
        end if
    end while
end function
  
// Main:
state = null
while 1
    if (state == null) then
        state = @GotoLowerLeft
    end if
    state = state
end while

@TonyLi , you’re right, MiniScript doesn’t do short-circuit evaluation yet. It’s on the to-do list, but for now you’d have to nest the if’s. (EDIT: Now done.)

I dig your Corner Lurker, though. In my test he actually defeated Blitzen, though for a while there I think it could have gone either way.

All right, people. The gauntlet is thrown. TonyLi is going to be the champion of the holiday Rumble if nobody can knock Corner Lurker and Blitzen off the pedestal. Let’s see what you’ve got!

Huh, that’s surprising. I ran a few rounds, and Corner Lurker fairly consistently got crushed by your Charger. Blitzen can beat Charger maybe 2 out of 3 times.

Any other takers? I wonder how a reindeer would fare that just drops mines in a grid pattern from left to right over the whole field.

Well, to be fair, I hadn’t included Charger in that particular rumble.

Tomorrow or Wednesday I’ll run a proper set of matches and see who comes out on top, including all the bots I’ve posted here as well as yours and any others that come up by then.

Indeed. They’d have to be fairly low-power mines, but that doesn’t matter if you bump into enough of them. I thought your Corner Lurker diagonal mine-laying was rather clever.

I also wonder about a reindeer that hides in a corner, and walls itself in with a series of high-energy mines. It’d be no defense against snowballs, of course, but could be really effective against chargers.

I’ve added short-circuit evaluation of and and or to MiniScript. So it is now safe to say things like “if see != null and see.distance < 10…”. (And there is a whole new set of integration tests to prove it.) Thanks to @TonyLi for pushing this one to the top of the to-do list!

The game itself has also received a little enhancement where, if your reindeer generates an error at runtime, you can see the full text of that error at the bottom of the script editor the next time you open that up.

1 Like

Hi @JoeStrout , I was messing around a little bit this morning and couldn’t figure out how to make good use of the map concatenation operator. I figured since we can add arbitrary key/values there would be a way to iterate over them but I can’t seem to figure it out!

It would be nice if the for loop could operate on map kind of like a Dictionary

map = {"hello":2, "world":3}
for word in map
  for i in range(1, word.value, 1)
    print(word.key)
  end for
end for
hello
hello
world
world
world

Also, I noticed the script editor doesn’t seem to complain about missing “end for” statements. That one tripped me up for a bit.

1 Like

Hi @eisenpony , thanks for trying it out! I’m not sure if you’re saying you’re unclear on how the map concatenation operator works — probably not, but just in case: map1 + map2 gives you a new map with all the key/value pairs of map2 added to map1. It’s probably not something you will need very often, and is included just for completeness.

And you’re right, there is currently no way to iterate over a map. I see what you’re suggesting: the “for…in” operator should give back little maps (mini-maps!) containing value and key. Makes perfect sense. I’ll put that on the to-do list!

Hmm, so it doesn’t! That surprises me a bit, but I guess it’s simply figuring there’s more input coming… and if you insist on running anyway, it will run until it reaches that point, and just stop.

So I will need to add something for the case where we claim to have given it a complete script, and if there are any unclosed block openers at that point, we throw an error.

Thanks for these excellent points!

@JoeStrout - It would be really helpful if you could report the line number of errors.

Tony, you’re scaring me… Give the rest of us a chance!